blob: 7141fb277096a6fbeea8939ac59d4f5edd71e079 [file] [log] [blame]
Xiaofei Baibca03e52021-09-09 09:42:37 +00001#!/usr/bin/env python3
2
3"""
Xiaofei Baibca03e52021-09-09 09:42:37 +00004This script is for comparing the size of the library files from two
5different Git revisions within an Mbed TLS repository.
6The results of the comparison is formatted as csv and stored at a
7configurable location.
8Note: must be run from Mbed TLS root.
9"""
10
11# Copyright The Mbed TLS Contributors
12# SPDX-License-Identifier: Apache-2.0
13#
14# Licensed under the Apache License, Version 2.0 (the "License"); you may
15# not use this file except in compliance with the License.
16# You may obtain a copy of the License at
17#
18# http://www.apache.org/licenses/LICENSE-2.0
19#
20# Unless required by applicable law or agreed to in writing, software
21# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
22# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23# See the License for the specific language governing permissions and
24# limitations under the License.
25
26import argparse
Yanray Wang21127f72023-07-19 12:09:45 +080027import logging
Xiaofei Baibca03e52021-09-09 09:42:37 +000028import os
Yanray Wang16ebc572023-05-30 18:10:20 +080029import re
Xiaofei Baibca03e52021-09-09 09:42:37 +000030import subprocess
31import sys
Yanray Wang16ebc572023-05-30 18:10:20 +080032import typing
Yanray Wang23bd5322023-05-24 11:03:59 +080033from enum import Enum
Xiaofei Baibca03e52021-09-09 09:42:37 +000034
Yanray Wang923f9432023-07-17 12:43:00 +080035from types import SimpleNamespace
Gilles Peskined9071e72022-09-18 21:17:09 +020036from mbedtls_dev import build_tree
Yanray Wang21127f72023-07-19 12:09:45 +080037from mbedtls_dev import logging_util
38from mbedtls_dev import typing_util
Gilles Peskined9071e72022-09-18 21:17:09 +020039
Yanray Wang23bd5322023-05-24 11:03:59 +080040class SupportedArch(Enum):
41 """Supported architecture for code size measurement."""
42 AARCH64 = 'aarch64'
43 AARCH32 = 'aarch32'
Yanray Wangaba71582023-05-29 16:45:56 +080044 ARMV8_M = 'armv8-m'
Yanray Wang23bd5322023-05-24 11:03:59 +080045 X86_64 = 'x86_64'
46 X86 = 'x86'
47
Yanray Wang386c2f92023-07-20 15:32:15 +080048CONFIG_TFM_MEDIUM_MBEDCRYPTO_H = '../configs/tfm_mbedcrypto_config_profile_medium.h'
49CONFIG_TFM_MEDIUM_PSA_CRYPTO_H = '../configs/crypto_config_profile_medium.h'
Yanray Wang6a862582023-05-24 12:24:38 +080050class SupportedConfig(Enum):
51 """Supported configuration for code size measurement."""
52 DEFAULT = 'default'
53 TFM_MEDIUM = 'tfm-medium'
54
Yanray Wang16ebc572023-05-30 18:10:20 +080055# Static library
56MBEDTLS_STATIC_LIB = {
57 'CRYPTO': 'library/libmbedcrypto.a',
58 'X509': 'library/libmbedx509.a',
59 'TLS': 'library/libmbedtls.a',
60}
61
Yanray Wang23bd5322023-05-24 11:03:59 +080062DETECT_ARCH_CMD = "cc -dM -E - < /dev/null"
63def detect_arch() -> str:
64 """Auto-detect host architecture."""
65 cc_output = subprocess.check_output(DETECT_ARCH_CMD, shell=True).decode()
Yanray Wang386c2f92023-07-20 15:32:15 +080066 if '__aarch64__' in cc_output:
Yanray Wang23bd5322023-05-24 11:03:59 +080067 return SupportedArch.AARCH64.value
Yanray Wang386c2f92023-07-20 15:32:15 +080068 if '__arm__' in cc_output:
Yanray Wang23bd5322023-05-24 11:03:59 +080069 return SupportedArch.AARCH32.value
Yanray Wang386c2f92023-07-20 15:32:15 +080070 if '__x86_64__' in cc_output:
Yanray Wang23bd5322023-05-24 11:03:59 +080071 return SupportedArch.X86_64.value
Yanray Wang386c2f92023-07-20 15:32:15 +080072 if '__x86__' in cc_output:
Yanray Wang23bd5322023-05-24 11:03:59 +080073 return SupportedArch.X86.value
74 else:
75 print("Unknown host architecture, cannot auto-detect arch.")
76 sys.exit(1)
Gilles Peskined9071e72022-09-18 21:17:09 +020077
Yanray Wang923f9432023-07-17 12:43:00 +080078class CodeSizeBuildInfo: # pylint: disable=too-few-public-methods
Yanray Wang6a862582023-05-24 12:24:38 +080079 """Gather information used to measure code size.
80
81 It collects information about architecture, configuration in order to
82 infer build command for code size measurement.
83 """
84
Yanray Wangc18cd892023-05-31 11:08:04 +080085 SupportedArchConfig = [
Yanray Wang386c2f92023-07-20 15:32:15 +080086 '-a ' + SupportedArch.AARCH64.value + ' -c ' + SupportedConfig.DEFAULT.value,
87 '-a ' + SupportedArch.AARCH32.value + ' -c ' + SupportedConfig.DEFAULT.value,
88 '-a ' + SupportedArch.X86_64.value + ' -c ' + SupportedConfig.DEFAULT.value,
89 '-a ' + SupportedArch.X86.value + ' -c ' + SupportedConfig.DEFAULT.value,
90 '-a ' + SupportedArch.ARMV8_M.value + ' -c ' + SupportedConfig.TFM_MEDIUM.value,
Yanray Wangc18cd892023-05-31 11:08:04 +080091 ]
92
Yanray Wang802af162023-07-17 14:04:30 +080093 def __init__(
94 self,
95 size_version: SimpleNamespace,
Yanray Wang21127f72023-07-19 12:09:45 +080096 host_arch: str,
97 logger: logging.Logger,
Yanray Wang802af162023-07-17 14:04:30 +080098 ) -> None:
Yanray Wang6a862582023-05-24 12:24:38 +080099 """
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800100 :param size_version:
101 SimpleNamespace containing info for code size measurement.
102 - size_version.arch: architecture to measure code size on.
103 - size_version.config: configuration type to measure code size
104 with.
105 :param host_arch: host architecture.
106 :param logger: logging module
Yanray Wang6a862582023-05-24 12:24:38 +0800107 """
Yanray Wang923f9432023-07-17 12:43:00 +0800108 self.size_version = size_version
Yanray Wang802af162023-07-17 14:04:30 +0800109 self.host_arch = host_arch
Yanray Wang21127f72023-07-19 12:09:45 +0800110 self.logger = logger
Yanray Wang6a862582023-05-24 12:24:38 +0800111
Yanray Wang923f9432023-07-17 12:43:00 +0800112 def infer_make_command(self) -> str:
Yanray Wang386c2f92023-07-20 15:32:15 +0800113 """Infer make command based on architecture and configuration."""
Yanray Wang6a862582023-05-24 12:24:38 +0800114
Yanray Wang386c2f92023-07-20 15:32:15 +0800115 # make command by default
Yanray Wang923f9432023-07-17 12:43:00 +0800116 if self.size_version.config == SupportedConfig.DEFAULT.value and \
Yanray Wang386c2f92023-07-20 15:32:15 +0800117 self.size_version.arch == self.host_arch:
Yanray Wang6a862582023-05-24 12:24:38 +0800118 return 'make -j lib CFLAGS=\'-Os \' '
Yanray Wang386c2f92023-07-20 15:32:15 +0800119 # make command for TF-M
Yanray Wang923f9432023-07-17 12:43:00 +0800120 elif self.size_version.arch == SupportedArch.ARMV8_M.value and \
121 self.size_version.config == SupportedConfig.TFM_MEDIUM.value:
Yanray Wang6a862582023-05-24 12:24:38 +0800122 return \
Yanray Wang60430bd2023-05-29 14:48:18 +0800123 'make -j lib CC=armclang \
Yanray Wang6a862582023-05-24 12:24:38 +0800124 CFLAGS=\'--target=arm-arm-none-eabi -mcpu=cortex-m33 -Os \
125 -DMBEDTLS_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_MBEDCRYPTO_H + '\\\" \
126 -DMBEDTLS_PSA_CRYPTO_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_PSA_CRYPTO_H + '\\\" \''
Yanray Wang386c2f92023-07-20 15:32:15 +0800127 # unsupported combinations
Yanray Wang6a862582023-05-24 12:24:38 +0800128 else:
Yanray Wang21127f72023-07-19 12:09:45 +0800129 self.logger.error("Unsupported combination of architecture: {} " \
130 "and configuration: {}.\n"
131 .format(self.size_version.arch,
132 self.size_version.config))
133 self.logger.info("Please use supported combination of " \
134 "architecture and configuration:")
Yanray Wang923f9432023-07-17 12:43:00 +0800135 for comb in CodeSizeBuildInfo.SupportedArchConfig:
Yanray Wang21127f72023-07-19 12:09:45 +0800136 self.logger.info(comb)
137 self.logger.info("")
138 self.logger.info("For your system, please use:")
Yanray Wang923f9432023-07-17 12:43:00 +0800139 for comb in CodeSizeBuildInfo.SupportedArchConfig:
Yanray Wang802af162023-07-17 14:04:30 +0800140 if "default" in comb and self.host_arch not in comb:
Yanray Wang21f17442023-06-01 11:29:06 +0800141 continue
Yanray Wang21127f72023-07-19 12:09:45 +0800142 self.logger.info(comb)
Yanray Wang6a862582023-05-24 12:24:38 +0800143 sys.exit(1)
144
145
Yanray Wange0e27602023-07-14 17:37:45 +0800146class CodeSizeCalculator:
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800147 """ A calculator to calculate code size of library/*.o based on
Yanray Wange0e27602023-07-14 17:37:45 +0800148 Git revision and code size measurement tool.
149 """
150
151 def __init__(
152 self,
153 revision: str,
154 make_cmd: str,
Yanray Wang21127f72023-07-19 12:09:45 +0800155 measure_cmd: str,
156 logger: logging.Logger,
Yanray Wange0e27602023-07-14 17:37:45 +0800157 ) -> None:
158 """
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800159 :param revision: Git revision.(E.g: commit)
160 :param make_cmd: command to build library/*.o.
161 :param measure_cmd: command to measure code size for library/*.o.
162 :param logger: logging module
Yanray Wange0e27602023-07-14 17:37:45 +0800163 """
164 self.repo_path = "."
165 self.git_command = "git"
166 self.make_clean = 'make clean'
167
168 self.revision = revision
169 self.make_cmd = make_cmd
Yanray Wang802af162023-07-17 14:04:30 +0800170 self.measure_cmd = measure_cmd
Yanray Wang21127f72023-07-19 12:09:45 +0800171 self.logger = logger
Yanray Wange0e27602023-07-14 17:37:45 +0800172
173 @staticmethod
Yanray Wang386c2f92023-07-20 15:32:15 +0800174 def validate_revision(revision: str) -> str:
Yanray Wange0e27602023-07-14 17:37:45 +0800175 result = subprocess.check_output(["git", "rev-parse", "--verify",
Yanray Wang386c2f92023-07-20 15:32:15 +0800176 revision + "^{commit}"], shell=False,
177 universal_newlines=True)
178 return result[:7]
Yanray Wange0e27602023-07-14 17:37:45 +0800179
Yanray Wang21127f72023-07-19 12:09:45 +0800180 def _create_git_worktree(self) -> str:
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800181 """Create a separate worktree for revision.
182 If revision is current, use current worktree instead."""
Yanray Wange0e27602023-07-14 17:37:45 +0800183
Yanray Wang21127f72023-07-19 12:09:45 +0800184 if self.revision == "current":
185 self.logger.debug("Using current work directory.")
Yanray Wange0e27602023-07-14 17:37:45 +0800186 git_worktree_path = self.repo_path
187 else:
Yanray Wang21127f72023-07-19 12:09:45 +0800188 self.logger.debug("Creating git worktree for {}."
189 .format(self.revision))
190 git_worktree_path = os.path.join(self.repo_path,
191 "temp-" + self.revision)
Yanray Wange0e27602023-07-14 17:37:45 +0800192 subprocess.check_output(
193 [self.git_command, "worktree", "add", "--detach",
Yanray Wang21127f72023-07-19 12:09:45 +0800194 git_worktree_path, self.revision], cwd=self.repo_path,
Yanray Wange0e27602023-07-14 17:37:45 +0800195 stderr=subprocess.STDOUT
196 )
197
198 return git_worktree_path
199
200 def _build_libraries(self, git_worktree_path: str) -> None:
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800201 """Build library/*.o in the specified worktree."""
Yanray Wange0e27602023-07-14 17:37:45 +0800202
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800203 self.logger.debug("Building library/*.o for {}."
Yanray Wang21127f72023-07-19 12:09:45 +0800204 .format(self.revision))
Yanray Wange0e27602023-07-14 17:37:45 +0800205 my_environment = os.environ.copy()
206 try:
207 subprocess.check_output(
208 self.make_clean, env=my_environment, shell=True,
209 cwd=git_worktree_path, stderr=subprocess.STDOUT,
Yanray Wang386c2f92023-07-20 15:32:15 +0800210 universal_newlines=True
Yanray Wange0e27602023-07-14 17:37:45 +0800211 )
212 subprocess.check_output(
213 self.make_cmd, env=my_environment, shell=True,
214 cwd=git_worktree_path, stderr=subprocess.STDOUT,
Yanray Wang386c2f92023-07-20 15:32:15 +0800215 universal_newlines=True
Yanray Wange0e27602023-07-14 17:37:45 +0800216 )
217 except subprocess.CalledProcessError as e:
218 self._handle_called_process_error(e, git_worktree_path)
219
Yanray Wang386c2f92023-07-20 15:32:15 +0800220 def _gen_raw_code_size(self, git_worktree_path: str) -> typing.Dict[str, str]:
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800221 """Measure code size by a tool and return in UTF-8 encoding."""
Yanray Wang21127f72023-07-19 12:09:45 +0800222
223 self.logger.debug("Measuring code size for {} by `{}`."
224 .format(self.revision,
225 self.measure_cmd.strip().split(' ')[0]))
Yanray Wange0e27602023-07-14 17:37:45 +0800226
227 res = {}
228 for mod, st_lib in MBEDTLS_STATIC_LIB.items():
229 try:
230 result = subprocess.check_output(
Yanray Wang802af162023-07-17 14:04:30 +0800231 [self.measure_cmd + ' ' + st_lib], cwd=git_worktree_path,
232 shell=True, universal_newlines=True
Yanray Wange0e27602023-07-14 17:37:45 +0800233 )
234 res[mod] = result
235 except subprocess.CalledProcessError as e:
236 self._handle_called_process_error(e, git_worktree_path)
237
238 return res
239
240 def _remove_worktree(self, git_worktree_path: str) -> None:
241 """Remove temporary worktree."""
242 if git_worktree_path != self.repo_path:
Yanray Wang21127f72023-07-19 12:09:45 +0800243 self.logger.debug("Removing temporary worktree {}."
244 .format(git_worktree_path))
Yanray Wange0e27602023-07-14 17:37:45 +0800245 subprocess.check_output(
246 [self.git_command, "worktree", "remove", "--force",
247 git_worktree_path], cwd=self.repo_path,
248 stderr=subprocess.STDOUT
249 )
250
251 def _handle_called_process_error(self, e: subprocess.CalledProcessError,
252 git_worktree_path: str) -> None:
253 """Handle a CalledProcessError and quit the program gracefully.
254 Remove any extra worktrees so that the script may be called again."""
255
256 # Tell the user what went wrong
Yanray Wang21127f72023-07-19 12:09:45 +0800257 self.logger.error(e, exc_info=True)
Yanray Wang386c2f92023-07-20 15:32:15 +0800258 self.logger.error("Process output:\n {}".format(e.output))
Yanray Wange0e27602023-07-14 17:37:45 +0800259
260 # Quit gracefully by removing the existing worktree
261 self._remove_worktree(git_worktree_path)
262 sys.exit(-1)
263
Yanray Wang386c2f92023-07-20 15:32:15 +0800264 def cal_libraries_code_size(self) -> typing.Dict[str, str]:
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800265 """Do a complete round to calculate code size of library/*.o
266 by measurement tool.
267
268 :return A dictionary of measured code size
269 - typing.Dict[mod: str]
270 """
Yanray Wange0e27602023-07-14 17:37:45 +0800271
Yanray Wang21127f72023-07-19 12:09:45 +0800272 git_worktree_path = self._create_git_worktree()
Yanray Wange0e27602023-07-14 17:37:45 +0800273 self._build_libraries(git_worktree_path)
Yanray Wang21127f72023-07-19 12:09:45 +0800274 res = self._gen_raw_code_size(git_worktree_path)
Yanray Wange0e27602023-07-14 17:37:45 +0800275 self._remove_worktree(git_worktree_path)
276
277 return res
278
279
Yanray Wang15c43f32023-07-17 11:17:12 +0800280class CodeSizeGenerator:
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800281 """ A generator based on size measurement tool for library/*.o.
Yanray Wang15c43f32023-07-17 11:17:12 +0800282
283 This is an abstract class. To use it, derive a class that implements
284 size_generator_write_record and size_generator_write_comparison methods,
285 then call both of them with proper arguments.
286 """
Yanray Wang21127f72023-07-19 12:09:45 +0800287 def __init__(self, logger: logging.Logger) -> None:
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800288 """
289 :param logger: logging module
290 """
Yanray Wang21127f72023-07-19 12:09:45 +0800291 self.logger = logger
292
Yanray Wang15c43f32023-07-17 11:17:12 +0800293 def size_generator_write_record(
294 self,
295 revision: str,
296 code_size_text: typing.Dict,
297 output_file: str
298 ) -> None:
299 """Write size record into a file.
300
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800301 :param revision: Git revision.(E.g: commit)
302 :param code_size_text:
303 string output (utf-8) from measurement tool of code size.
304 - typing.Dict[mod: str]
305 :param output_file: file which the code size record is written to.
Yanray Wang15c43f32023-07-17 11:17:12 +0800306 """
307 raise NotImplementedError
308
309 def size_generator_write_comparison(
310 self,
311 old_rev: str,
312 new_rev: str,
Yanray Wang386c2f92023-07-20 15:32:15 +0800313 output_stream: str,
Yanray Wang227576a2023-07-18 14:35:05 +0800314 result_options: SimpleNamespace
Yanray Wang15c43f32023-07-17 11:17:12 +0800315 ) -> None:
316 """Write a comparision result into a stream between two revisions.
317
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800318 :param old_rev: old Git revision to compared with.
319 :param new_rev: new Git revision to compared with.
320 :param output_stream: stream which the code size record is written to.
321 :param result_options:
322 SimpleNamespace containing options for comparison result.
323 - result_options.with_markdown: write comparision result in a
324 markdown table. (Default: False)
325 - result_options.stdout: direct comparison result into
326 sys.stdout. (Default: False)
Yanray Wang15c43f32023-07-17 11:17:12 +0800327 """
328 raise NotImplementedError
329
330
331class CodeSizeGeneratorWithSize(CodeSizeGenerator):
Yanray Wang16ebc572023-05-30 18:10:20 +0800332 """Code Size Base Class for size record saving and writing."""
333
Yanray Wangfc6ed4d2023-07-14 17:33:09 +0800334 class SizeEntry: # pylint: disable=too-few-public-methods
335 """Data Structure to only store information of code size."""
336 def __init__(self, text, data, bss, dec):
337 self.text = text
338 self.data = data
339 self.bss = bss
340 self.total = dec # total <=> dec
341
Yanray Wang21127f72023-07-19 12:09:45 +0800342 def __init__(self, logger: logging.Logger) -> None:
Yanray Wang16ebc572023-05-30 18:10:20 +0800343 """ Variable code_size is used to store size info for any revisions.
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800344 :param code_size:
345 Data Format as following:
346 {revision: {module: {file_name: [text, data, bss, dec],
347 etc ...
348 },
349 etc ...
350 },
351 etc ...
352 }
Yanray Wang16ebc572023-05-30 18:10:20 +0800353 """
Yanray Wang21127f72023-07-19 12:09:45 +0800354 super().__init__(logger)
Yanray Wang16ebc572023-05-30 18:10:20 +0800355 self.code_size = {} #type: typing.Dict[str, typing.Dict]
356
Yanray Wang386c2f92023-07-20 15:32:15 +0800357 def _set_size_record(self, revision: str, mod: str, size_text: str) -> None:
Yanray Wang16ebc572023-05-30 18:10:20 +0800358 """Store size information for target revision and high-level module.
359
360 size_text Format: text data bss dec hex filename
361 """
362 size_record = {}
363 for line in size_text.splitlines()[1:]:
364 data = line.split()
Yanray Wang9b174e92023-07-17 17:59:53 +0800365 # file_name: SizeEntry(text, data, bss, dec)
366 size_record[data[5]] = CodeSizeGeneratorWithSize.SizeEntry(
367 data[0], data[1], data[2], data[3])
Yanray Wang16ebc572023-05-30 18:10:20 +0800368 if revision in self.code_size:
369 self.code_size[revision].update({mod: size_record})
370 else:
371 self.code_size[revision] = {mod: size_record}
372
373 def read_size_record(self, revision: str, fname: str) -> None:
374 """Read size information from csv file and write it into code_size.
375
376 fname Format: filename text data bss dec
377 """
378 mod = ""
379 size_record = {}
380 with open(fname, 'r') as csv_file:
381 for line in csv_file:
382 data = line.strip().split()
383 # check if we find the beginning of a module
384 if data and data[0] in MBEDTLS_STATIC_LIB:
385 mod = data[0]
386 continue
387
388 if mod:
Yanray Wang9b174e92023-07-17 17:59:53 +0800389 # file_name: SizeEntry(text, data, bss, dec)
390 size_record[data[0]] = CodeSizeGeneratorWithSize.SizeEntry(
Yanray Wangfc6ed4d2023-07-14 17:33:09 +0800391 data[1], data[2], data[3], data[4])
Yanray Wang16ebc572023-05-30 18:10:20 +0800392
393 # check if we hit record for the end of a module
394 m = re.match(r'.?TOTALS', line)
395 if m:
396 if revision in self.code_size:
397 self.code_size[revision].update({mod: size_record})
398 else:
399 self.code_size[revision] = {mod: size_record}
400 mod = ""
401 size_record = {}
402
403 def _size_reader_helper(
404 self,
405 revision: str,
Yanray Wangb664cb72023-07-18 12:28:35 +0800406 output: typing_util.Writable,
407 with_markdown=False
Yanray Wang16ebc572023-05-30 18:10:20 +0800408 ) -> typing.Iterator[tuple]:
409 """A helper function to peel code_size based on revision."""
410 for mod, file_size in self.code_size[revision].items():
Yanray Wangb664cb72023-07-18 12:28:35 +0800411 if not with_markdown:
412 output.write("\n" + mod + "\n")
Yanray Wang16ebc572023-05-30 18:10:20 +0800413 for fname, size_entry in file_size.items():
414 yield mod, fname, size_entry
415
Yanray Wang386c2f92023-07-20 15:32:15 +0800416 def _write_size_record(
Yanray Wang16ebc572023-05-30 18:10:20 +0800417 self,
418 revision: str,
419 output: typing_util.Writable
420 ) -> None:
421 """Write size information to a file.
422
423 Writing Format: file_name text data bss total(dec)
424 """
Yanray Wangb664cb72023-07-18 12:28:35 +0800425 format_string = "{:<30} {:>7} {:>7} {:>7} {:>7}\n"
426 output.write(format_string.format("filename",
427 "text", "data", "bss", "total"))
Yanray Wang16ebc572023-05-30 18:10:20 +0800428 for _, fname, size_entry in self._size_reader_helper(revision, output):
Yanray Wangb664cb72023-07-18 12:28:35 +0800429 output.write(format_string.format(fname,
430 size_entry.text, size_entry.data,
431 size_entry.bss, size_entry.total))
Yanray Wang16ebc572023-05-30 18:10:20 +0800432
Yanray Wang386c2f92023-07-20 15:32:15 +0800433 def _write_comparison(
Yanray Wang16ebc572023-05-30 18:10:20 +0800434 self,
435 old_rev: str,
436 new_rev: str,
Yanray Wangb664cb72023-07-18 12:28:35 +0800437 output: typing_util.Writable,
438 with_markdown: bool
Yanray Wang16ebc572023-05-30 18:10:20 +0800439 ) -> None:
440 """Write comparison result into a file.
441
Yanray Wang9b174e92023-07-17 17:59:53 +0800442 Writing Format: file_name current(text,data) old(text,data)\
443 change(text,data) change_pct%(text,data)
Yanray Wang16ebc572023-05-30 18:10:20 +0800444 """
Yanray Wang9b174e92023-07-17 17:59:53 +0800445
446 def cal_size_section_variation(mod, fname, size_entry, attr):
447 new_size = int(size_entry.__dict__[attr])
Yanray Wang16ebc572023-05-30 18:10:20 +0800448 # check if we have the file in old revision
449 if fname in self.code_size[old_rev][mod]:
Yanray Wang9b174e92023-07-17 17:59:53 +0800450 old_size = int(self.code_size[old_rev][mod][fname].__dict__[attr])
Yanray Wang16ebc572023-05-30 18:10:20 +0800451 change = new_size - old_size
452 if old_size != 0:
453 change_pct = change / old_size
454 else:
455 change_pct = 0
Yanray Wang9b174e92023-07-17 17:59:53 +0800456 return [new_size, old_size, change, change_pct]
Yanray Wang16ebc572023-05-30 18:10:20 +0800457 else:
Yanray Wang9b174e92023-07-17 17:59:53 +0800458 return [new_size]
459
Yanray Wangb664cb72023-07-18 12:28:35 +0800460 if with_markdown:
461 format_string = "| {:<30} | {:<18} | {:<14} | {:<17} | {:<18} |\n"
462 else:
463 format_string = "{:<30} {:<18} {:<14} {:<17} {:<18}\n"
464
Yanray Wang386c2f92023-07-20 15:32:15 +0800465 output.write(format_string
466 .format("filename",
467 "current(text,data)", "old(text,data)",
468 "change(text,data)", "change%(text,data)"))
Yanray Wangb664cb72023-07-18 12:28:35 +0800469 if with_markdown:
470 output.write(format_string
471 .format("----:", "----:", "----:", "----:", "----:"))
472
Yanray Wang386c2f92023-07-20 15:32:15 +0800473 for mod, fname, size_entry in \
Yanray Wangb664cb72023-07-18 12:28:35 +0800474 self._size_reader_helper(new_rev, output, with_markdown):
475 text_vari = cal_size_section_variation(mod, fname,
476 size_entry, 'text')
477 data_vari = cal_size_section_variation(mod, fname,
478 size_entry, 'data')
Yanray Wang9b174e92023-07-17 17:59:53 +0800479
480 if len(text_vari) != 1:
Yanray Wangb664cb72023-07-18 12:28:35 +0800481 # skip the files that haven't changed in code size if we write
482 # comparison result in a markdown table.
483 if with_markdown and text_vari[2] == 0 and data_vari[2] == 0:
484 continue
Yanray Wang386c2f92023-07-20 15:32:15 +0800485 output.write(
486 format_string
487 .format(fname,
488 str(text_vari[0]) + "," + str(data_vari[0]),
489 str(text_vari[1]) + "," + str(data_vari[1]),
490 str(text_vari[2]) + "," + str(data_vari[2]),
491 "{:.2%}".format(text_vari[3]) + ","
492 + "{:.2%}".format(data_vari[3])))
Yanray Wang9b174e92023-07-17 17:59:53 +0800493 else:
Yanray Wang386c2f92023-07-20 15:32:15 +0800494 output.write("{:<30} {:<18}\n"
495 .format(fname,
496 str(text_vari[0]) + "," + str(data_vari[0])))
Yanray Wang16ebc572023-05-30 18:10:20 +0800497
Yanray Wang15c43f32023-07-17 11:17:12 +0800498 def size_generator_write_record(
499 self,
500 revision: str,
501 code_size_text: typing.Dict,
502 output_file: str
503 ) -> None:
504 """Write size record into a specified file based on Git revision and
505 output from `size` tool."""
Yanray Wang21127f72023-07-19 12:09:45 +0800506 self.logger.debug("Generating code size csv for {}.".format(revision))
507
Yanray Wang15c43f32023-07-17 11:17:12 +0800508 for mod, size_text in code_size_text.items():
Yanray Wang386c2f92023-07-20 15:32:15 +0800509 self._set_size_record(revision, mod, size_text)
Yanray Wang15c43f32023-07-17 11:17:12 +0800510
Yanray Wang15c43f32023-07-17 11:17:12 +0800511 output = open(output_file, "w")
Yanray Wang386c2f92023-07-20 15:32:15 +0800512 self._write_size_record(revision, output)
Yanray Wang15c43f32023-07-17 11:17:12 +0800513
514 def size_generator_write_comparison(
515 self,
516 old_rev: str,
517 new_rev: str,
Yanray Wang386c2f92023-07-20 15:32:15 +0800518 output_stream: str,
Yanray Wang227576a2023-07-18 14:35:05 +0800519 result_options: SimpleNamespace
Yanray Wang15c43f32023-07-17 11:17:12 +0800520 ) -> None:
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800521 """Write a comparision result into a stream between two revisions.
522
523 By default, it's written into a file called output_stream.
524 Once result_options.stdout is set, it's written into sys.stdout instead.
525 """
Yanray Wang21127f72023-07-19 12:09:45 +0800526 self.logger.debug("Generating comparison results between {} and {}."
527 .format(old_rev, new_rev))
528
Yanray Wang227576a2023-07-18 14:35:05 +0800529 if result_options.stdout:
530 output = sys.stdout
531 else:
532 output = open(output_stream, "w")
Yanray Wang386c2f92023-07-20 15:32:15 +0800533 self._write_comparison(old_rev, new_rev, output,
534 result_options.with_markdown)
Yanray Wang15c43f32023-07-17 11:17:12 +0800535
Yanray Wang16ebc572023-05-30 18:10:20 +0800536
Yanray Wangfc6ed4d2023-07-14 17:33:09 +0800537class CodeSizeComparison:
Xiaofei Bai2400b502021-10-21 12:22:58 +0000538 """Compare code size between two Git revisions."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000539
Yanray Wang72b105f2023-05-31 15:20:39 +0800540 def __init__(
541 self,
Yanray Wang923f9432023-07-17 12:43:00 +0800542 old_size_version: SimpleNamespace,
543 new_size_version: SimpleNamespace,
Yanray Wang802af162023-07-17 14:04:30 +0800544 code_size_common: SimpleNamespace,
Yanray Wang21127f72023-07-19 12:09:45 +0800545 logger: logging.Logger,
Yanray Wang72b105f2023-05-31 15:20:39 +0800546 ) -> None:
Xiaofei Baibca03e52021-09-09 09:42:37 +0000547 """
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800548 :param old_size_version: SimpleNamespace containing old version info
549 to compare code size with.
550 :param new_size_version: SimpleNamespace containing new version info
551 to take as comparision base.
552 :param code_size_common: SimpleNamespace containing common info for
553 both old and new size version,
554 measurement tool and result options.
555 :param logger: logging module
Xiaofei Baibca03e52021-09-09 09:42:37 +0000556 """
Yanray Wang386c2f92023-07-20 15:32:15 +0800557 self.result_dir = os.path.abspath(
558 code_size_common.result_options.result_dir)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000559 os.makedirs(self.result_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000560
561 self.csv_dir = os.path.abspath("code_size_records/")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000562 os.makedirs(self.csv_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000563
Yanray Wang21127f72023-07-19 12:09:45 +0800564 self.logger = logger
565
Yanray Wang923f9432023-07-17 12:43:00 +0800566 self.old_size_version = old_size_version
567 self.new_size_version = new_size_version
Yanray Wang802af162023-07-17 14:04:30 +0800568 self.code_size_common = code_size_common
Yanray Wang386c2f92023-07-20 15:32:15 +0800569 # infer make command
Yanray Wang21127f72023-07-19 12:09:45 +0800570 self.old_size_version.make_cmd = CodeSizeBuildInfo(
571 self.old_size_version, self.code_size_common.host_arch,
572 self.logger).infer_make_command()
573 self.new_size_version.make_cmd = CodeSizeBuildInfo(
574 self.new_size_version, self.code_size_common.host_arch,
575 self.logger).infer_make_command()
Yanray Wang386c2f92023-07-20 15:32:15 +0800576 # initialize size parser with corresponding measurement tool
Yanray Wang21127f72023-07-19 12:09:45 +0800577 self.code_size_generator = self.__generate_size_parser()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000578
Yanray Wang21127f72023-07-19 12:09:45 +0800579 def __generate_size_parser(self):
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800580 """Generate a parser for the corresponding measurement tool."""
Yanray Wang21127f72023-07-19 12:09:45 +0800581 if re.match(r'size', self.code_size_common.measure_cmd.strip()):
582 return CodeSizeGeneratorWithSize(self.logger)
Yanray Wang802af162023-07-17 14:04:30 +0800583 else:
Yanray Wang21127f72023-07-19 12:09:45 +0800584 self.logger.error("Unsupported measurement tool: `{}`."
585 .format(self.code_size_common.measure_cmd
586 .strip().split(' ')[0]))
Yanray Wang802af162023-07-17 14:04:30 +0800587 sys.exit(1)
588
589
Yanray Wang386c2f92023-07-20 15:32:15 +0800590 def cal_code_size(
591 self,
592 size_version: SimpleNamespace
593 ) -> typing.Dict[str, str]:
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800594 """Calculate code size of library/*.o in a UTF-8 encoding"""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000595
Yanray Wang21127f72023-07-19 12:09:45 +0800596 return CodeSizeCalculator(size_version.revision, size_version.make_cmd,
597 self.code_size_common.measure_cmd,
598 self.logger).cal_libraries_code_size()
Yanray Wang8804db92023-05-30 18:18:18 +0800599
Yanray Wang386c2f92023-07-20 15:32:15 +0800600 def gen_file_name(
601 self,
602 old_size_version: SimpleNamespace,
603 new_size_version=None
604 ) -> str:
Yanray Wang21127f72023-07-19 12:09:45 +0800605 """Generate a literal string as csv file name."""
Yanray Wang923f9432023-07-17 12:43:00 +0800606 if new_size_version:
Yanray Wang802af162023-07-17 14:04:30 +0800607 return '{}-{}-{}-{}-{}-{}-{}.csv'\
Yanray Wang386c2f92023-07-20 15:32:15 +0800608 .format(old_size_version.revision, old_size_version.arch,
609 old_size_version.config,
610 new_size_version.revision, new_size_version.arch,
611 new_size_version.config,
612 self.code_size_common.measure_cmd.strip()\
613 .split(' ')[0])
Yanray Wang923f9432023-07-17 12:43:00 +0800614 else:
Yanray Wang802af162023-07-17 14:04:30 +0800615 return '{}-{}-{}-{}.csv'\
Yanray Wang386c2f92023-07-20 15:32:15 +0800616 .format(old_size_version.revision, old_size_version.arch,
617 old_size_version.config,
618 self.code_size_common.measure_cmd.strip()\
619 .split(' ')[0])
Yanray Wang923f9432023-07-17 12:43:00 +0800620
Yanray Wang386c2f92023-07-20 15:32:15 +0800621 def gen_code_size_report(self, size_version: SimpleNamespace) -> None:
Yanray Wang5e9130a2023-07-17 11:55:54 +0800622 """Generate code size record and write it into a file."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000623
Yanray Wang21127f72023-07-19 12:09:45 +0800624 self.logger.info("Start to generate code size record for {}."
625 .format(size_version.revision))
626 output_file = os.path.join(self.csv_dir,
627 self.gen_file_name(size_version))
Xiaofei Baibca03e52021-09-09 09:42:37 +0000628 # Check if the corresponding record exists
Yanray Wang21127f72023-07-19 12:09:45 +0800629 if size_version.revision != "current" and \
630 os.path.exists(output_file):
631 self.logger.debug("Code size csv file for {} already exists."
632 .format(size_version.revision))
633 self.code_size_generator.read_size_record(
634 size_version.revision, output_file)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000635 else:
Yanray Wang386c2f92023-07-20 15:32:15 +0800636 self.code_size_generator.size_generator_write_record(
637 size_version.revision, self.cal_code_size(size_version),
638 output_file)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000639
Yanray Wang386c2f92023-07-20 15:32:15 +0800640 def gen_code_size_comparison(self) -> None:
Yanray Wang5e9130a2023-07-17 11:55:54 +0800641 """Generate results of code size changes between two revisions,
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800642 old and new.
643
644 - Measured code size results of these two revisions must be available.
645 - The result is directed into either file / stdout depending on
646 the option, code_size_common.result_options.stdout. (Default: file)
647 """
Xiaofei Baibca03e52021-09-09 09:42:37 +0000648
Yanray Wang21127f72023-07-19 12:09:45 +0800649 self.logger.info("Start to generate comparision result between "\
650 "{} and {}."
651 .format(self.old_size_version.revision,
652 self.new_size_version.revision))
653 output_file = os.path.join(
654 self.result_dir,
655 self.gen_file_name(self.old_size_version, self.new_size_version))
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000656
Yanray Wang21127f72023-07-19 12:09:45 +0800657 self.code_size_generator.size_generator_write_comparison(
658 self.old_size_version.revision, self.new_size_version.revision,
659 output_file, self.code_size_common.result_options)
660
Yanray Wang386c2f92023-07-20 15:32:15 +0800661 def get_comparision_results(self) -> None:
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800662 """Compare size of library/*.o between self.old_size_version and
663 self.old_size_version and generate the result file."""
Gilles Peskined9071e72022-09-18 21:17:09 +0200664 build_tree.check_repo_path()
Yanray Wang923f9432023-07-17 12:43:00 +0800665 self.gen_code_size_report(self.old_size_version)
666 self.gen_code_size_report(self.new_size_version)
Yanray Wang386c2f92023-07-20 15:32:15 +0800667 self.gen_code_size_comparison()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000668
Yanray Wang923f9432023-07-17 12:43:00 +0800669
Xiaofei Bai2400b502021-10-21 12:22:58 +0000670def main():
Yanray Wang502c54f2023-05-31 11:41:36 +0800671 parser = argparse.ArgumentParser(description=(__doc__))
672 group_required = parser.add_argument_group(
673 'required arguments',
674 'required arguments to parse for running ' + os.path.basename(__file__))
675 group_required.add_argument(
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800676 '-o', '--old-rev', type=str, required=True,
677 help='old revision for comparison.')
Yanray Wang502c54f2023-05-31 11:41:36 +0800678
679 group_optional = parser.add_argument_group(
680 'optional arguments',
681 'optional arguments to parse for running ' + os.path.basename(__file__))
682 group_optional.add_argument(
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800683 '-r', '--result-dir', type=str, default='comparison',
684 help='directory where comparison result is stored. '
685 '(Default: comparison)')
Yanray Wang502c54f2023-05-31 11:41:36 +0800686 group_optional.add_argument(
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800687 '-n', '--new-rev', type=str, default=None,
688 help='new revision as comparison base. '
689 '(Default is the current work directory, including uncommitted '
690 'changes.)')
Yanray Wang502c54f2023-05-31 11:41:36 +0800691 group_optional.add_argument(
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800692 '-a', '--arch', type=str, default=detect_arch(),
Yanray Wang23bd5322023-05-24 11:03:59 +0800693 choices=list(map(lambda s: s.value, SupportedArch)),
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800694 help='Specify architecture for code size comparison. '
695 '(Default is the host architecture.)')
Yanray Wang502c54f2023-05-31 11:41:36 +0800696 group_optional.add_argument(
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800697 '-c', '--config', type=str, default=SupportedConfig.DEFAULT.value,
Yanray Wang6a862582023-05-24 12:24:38 +0800698 choices=list(map(lambda s: s.value, SupportedConfig)),
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800699 help='Specify configuration type for code size comparison. '
700 '(Default is the current MbedTLS configuration.)')
Yanray Wangb664cb72023-07-18 12:28:35 +0800701 group_optional.add_argument(
702 '--markdown', action='store_true', dest='markdown',
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800703 help='Show comparision of code size in a markdown table. '
704 '(Only show the files that have changed).')
Yanray Wang227576a2023-07-18 14:35:05 +0800705 group_optional.add_argument(
706 '--stdout', action='store_true', dest='stdout',
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800707 help='Set this option to direct comparison result into sys.stdout. '
708 '(Default: file)')
Yanray Wang21127f72023-07-19 12:09:45 +0800709 group_optional.add_argument(
710 '--verbose', action='store_true', dest='verbose',
Yanray Wang5b64e4c2023-07-20 15:09:51 +0800711 help='Show logs in detail for code size measurement. '
712 '(Default: False)')
Xiaofei Baibca03e52021-09-09 09:42:37 +0000713 comp_args = parser.parse_args()
714
Yanray Wang21127f72023-07-19 12:09:45 +0800715 logger = logging.getLogger()
716 logging_util.configure_logger(logger)
717 logger.setLevel(logging.DEBUG if comp_args.verbose else logging.INFO)
718
Xiaofei Baibca03e52021-09-09 09:42:37 +0000719 if os.path.isfile(comp_args.result_dir):
Yanray Wang21127f72023-07-19 12:09:45 +0800720 logger.error("{} is not a directory".format(comp_args.result_dir))
Xiaofei Baibca03e52021-09-09 09:42:37 +0000721 parser.exit()
722
Yanray Wang386c2f92023-07-20 15:32:15 +0800723 old_revision = CodeSizeCalculator.validate_revision(comp_args.old_rev)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000724 if comp_args.new_rev is not None:
Yanray Wang386c2f92023-07-20 15:32:15 +0800725 new_revision = CodeSizeCalculator.validate_revision(comp_args.new_rev)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000726 else:
727 new_revision = "current"
Xiaofei Bai2400b502021-10-21 12:22:58 +0000728
Yanray Wang923f9432023-07-17 12:43:00 +0800729 old_size_version = SimpleNamespace(
Yanray Wang386c2f92023-07-20 15:32:15 +0800730 version='old',
Yanray Wang923f9432023-07-17 12:43:00 +0800731 revision=old_revision,
732 config=comp_args.config,
733 arch=comp_args.arch,
Yanray Wang923f9432023-07-17 12:43:00 +0800734 make_cmd='',
735 )
736 new_size_version = SimpleNamespace(
Yanray Wang386c2f92023-07-20 15:32:15 +0800737 version='new',
Yanray Wang923f9432023-07-17 12:43:00 +0800738 revision=new_revision,
739 config=comp_args.config,
740 arch=comp_args.arch,
Yanray Wang923f9432023-07-17 12:43:00 +0800741 make_cmd='',
742 )
Yanray Wang802af162023-07-17 14:04:30 +0800743 code_size_common = SimpleNamespace(
Yanray Wang227576a2023-07-18 14:35:05 +0800744 result_options=SimpleNamespace(
745 result_dir=comp_args.result_dir,
746 with_markdown=comp_args.markdown,
747 stdout=comp_args.stdout,
748 ),
Yanray Wang802af162023-07-17 14:04:30 +0800749 host_arch=detect_arch(),
750 measure_cmd='size -t',
751 )
Yanray Wang923f9432023-07-17 12:43:00 +0800752
Yanray Wang21127f72023-07-19 12:09:45 +0800753 logger.info("Measure code size between {}:{}-{} and {}:{}-{} by `{}`."
754 .format(old_size_version.revision, old_size_version.config,
755 old_size_version.arch,
756 new_size_version.revision, old_size_version.config,
757 new_size_version.arch,
758 code_size_common.measure_cmd.strip().split(' ')[0]))
Yanray Wang386c2f92023-07-20 15:32:15 +0800759 CodeSizeComparison(old_size_version, new_size_version,
760 code_size_common, logger).get_comparision_results()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000761
Xiaofei Baibca03e52021-09-09 09:42:37 +0000762if __name__ == "__main__":
Xiaofei Bai2400b502021-10-21 12:22:58 +0000763 main()