blob: 01d7b165c651a467d1cd983216d3dae8e62d1bae [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 Wang923f9432023-07-17 12:43:00 +0800100 size_version: SimpleNamespace containing info for code size measurement.
101 size_version.arch: architecture to measure code size on.
102 size_version.config: configuration type to measure code size with.
Yanray Wang802af162023-07-17 14:04:30 +0800103 host_arch: host architecture.
Yanray Wang6a862582023-05-24 12:24:38 +0800104 """
Yanray Wang923f9432023-07-17 12:43:00 +0800105 self.size_version = size_version
Yanray Wang802af162023-07-17 14:04:30 +0800106 self.host_arch = host_arch
Yanray Wang21127f72023-07-19 12:09:45 +0800107 self.logger = logger
Yanray Wang6a862582023-05-24 12:24:38 +0800108
Yanray Wang923f9432023-07-17 12:43:00 +0800109 def infer_make_command(self) -> str:
Yanray Wang386c2f92023-07-20 15:32:15 +0800110 """Infer make command based on architecture and configuration."""
Yanray Wang6a862582023-05-24 12:24:38 +0800111
Yanray Wang386c2f92023-07-20 15:32:15 +0800112 # make command by default
Yanray Wang923f9432023-07-17 12:43:00 +0800113 if self.size_version.config == SupportedConfig.DEFAULT.value and \
Yanray Wang386c2f92023-07-20 15:32:15 +0800114 self.size_version.arch == self.host_arch:
Yanray Wang6a862582023-05-24 12:24:38 +0800115 return 'make -j lib CFLAGS=\'-Os \' '
Yanray Wang386c2f92023-07-20 15:32:15 +0800116 # make command for TF-M
Yanray Wang923f9432023-07-17 12:43:00 +0800117 elif self.size_version.arch == SupportedArch.ARMV8_M.value and \
118 self.size_version.config == SupportedConfig.TFM_MEDIUM.value:
Yanray Wang6a862582023-05-24 12:24:38 +0800119 return \
Yanray Wang60430bd2023-05-29 14:48:18 +0800120 'make -j lib CC=armclang \
Yanray Wang6a862582023-05-24 12:24:38 +0800121 CFLAGS=\'--target=arm-arm-none-eabi -mcpu=cortex-m33 -Os \
122 -DMBEDTLS_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_MBEDCRYPTO_H + '\\\" \
123 -DMBEDTLS_PSA_CRYPTO_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_PSA_CRYPTO_H + '\\\" \''
Yanray Wang386c2f92023-07-20 15:32:15 +0800124 # unsupported combinations
Yanray Wang6a862582023-05-24 12:24:38 +0800125 else:
Yanray Wang21127f72023-07-19 12:09:45 +0800126 self.logger.error("Unsupported combination of architecture: {} " \
127 "and configuration: {}.\n"
128 .format(self.size_version.arch,
129 self.size_version.config))
130 self.logger.info("Please use supported combination of " \
131 "architecture and configuration:")
Yanray Wang923f9432023-07-17 12:43:00 +0800132 for comb in CodeSizeBuildInfo.SupportedArchConfig:
Yanray Wang21127f72023-07-19 12:09:45 +0800133 self.logger.info(comb)
134 self.logger.info("")
135 self.logger.info("For your system, please use:")
Yanray Wang923f9432023-07-17 12:43:00 +0800136 for comb in CodeSizeBuildInfo.SupportedArchConfig:
Yanray Wang802af162023-07-17 14:04:30 +0800137 if "default" in comb and self.host_arch not in comb:
Yanray Wang21f17442023-06-01 11:29:06 +0800138 continue
Yanray Wang21127f72023-07-19 12:09:45 +0800139 self.logger.info(comb)
Yanray Wang6a862582023-05-24 12:24:38 +0800140 sys.exit(1)
141
142
Yanray Wange0e27602023-07-14 17:37:45 +0800143class CodeSizeCalculator:
144 """ A calculator to calculate code size of library objects based on
145 Git revision and code size measurement tool.
146 """
147
148 def __init__(
149 self,
150 revision: str,
151 make_cmd: str,
Yanray Wang21127f72023-07-19 12:09:45 +0800152 measure_cmd: str,
153 logger: logging.Logger,
Yanray Wange0e27602023-07-14 17:37:45 +0800154 ) -> None:
155 """
156 revision: Git revision.(E.g: commit)
Yanray Wang802af162023-07-17 14:04:30 +0800157 make_cmd: command to build objects in library.
158 measure_cmd: command to measure code size for objects in library.
Yanray Wange0e27602023-07-14 17:37:45 +0800159 """
160 self.repo_path = "."
161 self.git_command = "git"
162 self.make_clean = 'make clean'
163
164 self.revision = revision
165 self.make_cmd = make_cmd
Yanray Wang802af162023-07-17 14:04:30 +0800166 self.measure_cmd = measure_cmd
Yanray Wang21127f72023-07-19 12:09:45 +0800167 self.logger = logger
Yanray Wange0e27602023-07-14 17:37:45 +0800168
169 @staticmethod
Yanray Wang386c2f92023-07-20 15:32:15 +0800170 def validate_revision(revision: str) -> str:
Yanray Wange0e27602023-07-14 17:37:45 +0800171 result = subprocess.check_output(["git", "rev-parse", "--verify",
Yanray Wang386c2f92023-07-20 15:32:15 +0800172 revision + "^{commit}"], shell=False,
173 universal_newlines=True)
174 return result[:7]
Yanray Wange0e27602023-07-14 17:37:45 +0800175
Yanray Wang21127f72023-07-19 12:09:45 +0800176 def _create_git_worktree(self) -> str:
Yanray Wange0e27602023-07-14 17:37:45 +0800177 """Make a separate worktree for revision.
178 Do not modify the current worktree."""
179
Yanray Wang21127f72023-07-19 12:09:45 +0800180 if self.revision == "current":
181 self.logger.debug("Using current work directory.")
Yanray Wange0e27602023-07-14 17:37:45 +0800182 git_worktree_path = self.repo_path
183 else:
Yanray Wang21127f72023-07-19 12:09:45 +0800184 self.logger.debug("Creating git worktree for {}."
185 .format(self.revision))
186 git_worktree_path = os.path.join(self.repo_path,
187 "temp-" + self.revision)
Yanray Wange0e27602023-07-14 17:37:45 +0800188 subprocess.check_output(
189 [self.git_command, "worktree", "add", "--detach",
Yanray Wang21127f72023-07-19 12:09:45 +0800190 git_worktree_path, self.revision], cwd=self.repo_path,
Yanray Wange0e27602023-07-14 17:37:45 +0800191 stderr=subprocess.STDOUT
192 )
193
194 return git_worktree_path
195
196 def _build_libraries(self, git_worktree_path: str) -> None:
197 """Build libraries in the specified worktree."""
198
Yanray Wang21127f72023-07-19 12:09:45 +0800199 self.logger.debug("Building objects of library for {}."
200 .format(self.revision))
Yanray Wange0e27602023-07-14 17:37:45 +0800201 my_environment = os.environ.copy()
202 try:
203 subprocess.check_output(
204 self.make_clean, env=my_environment, shell=True,
205 cwd=git_worktree_path, stderr=subprocess.STDOUT,
Yanray Wang386c2f92023-07-20 15:32:15 +0800206 universal_newlines=True
Yanray Wange0e27602023-07-14 17:37:45 +0800207 )
208 subprocess.check_output(
209 self.make_cmd, env=my_environment, shell=True,
210 cwd=git_worktree_path, stderr=subprocess.STDOUT,
Yanray Wang386c2f92023-07-20 15:32:15 +0800211 universal_newlines=True
Yanray Wange0e27602023-07-14 17:37:45 +0800212 )
213 except subprocess.CalledProcessError as e:
214 self._handle_called_process_error(e, git_worktree_path)
215
Yanray Wang386c2f92023-07-20 15:32:15 +0800216 def _gen_raw_code_size(self, git_worktree_path: str) -> typing.Dict[str, str]:
Yanray Wange0e27602023-07-14 17:37:45 +0800217 """Calculate code size with measurement tool in UTF-8 encoding."""
Yanray Wang21127f72023-07-19 12:09:45 +0800218
219 self.logger.debug("Measuring code size for {} by `{}`."
220 .format(self.revision,
221 self.measure_cmd.strip().split(' ')[0]))
Yanray Wange0e27602023-07-14 17:37:45 +0800222
223 res = {}
224 for mod, st_lib in MBEDTLS_STATIC_LIB.items():
225 try:
226 result = subprocess.check_output(
Yanray Wang802af162023-07-17 14:04:30 +0800227 [self.measure_cmd + ' ' + st_lib], cwd=git_worktree_path,
228 shell=True, universal_newlines=True
Yanray Wange0e27602023-07-14 17:37:45 +0800229 )
230 res[mod] = result
231 except subprocess.CalledProcessError as e:
232 self._handle_called_process_error(e, git_worktree_path)
233
234 return res
235
236 def _remove_worktree(self, git_worktree_path: str) -> None:
237 """Remove temporary worktree."""
238 if git_worktree_path != self.repo_path:
Yanray Wang21127f72023-07-19 12:09:45 +0800239 self.logger.debug("Removing temporary worktree {}."
240 .format(git_worktree_path))
Yanray Wange0e27602023-07-14 17:37:45 +0800241 subprocess.check_output(
242 [self.git_command, "worktree", "remove", "--force",
243 git_worktree_path], cwd=self.repo_path,
244 stderr=subprocess.STDOUT
245 )
246
247 def _handle_called_process_error(self, e: subprocess.CalledProcessError,
248 git_worktree_path: str) -> None:
249 """Handle a CalledProcessError and quit the program gracefully.
250 Remove any extra worktrees so that the script may be called again."""
251
252 # Tell the user what went wrong
Yanray Wang21127f72023-07-19 12:09:45 +0800253 self.logger.error(e, exc_info=True)
Yanray Wang386c2f92023-07-20 15:32:15 +0800254 self.logger.error("Process output:\n {}".format(e.output))
Yanray Wange0e27602023-07-14 17:37:45 +0800255
256 # Quit gracefully by removing the existing worktree
257 self._remove_worktree(git_worktree_path)
258 sys.exit(-1)
259
Yanray Wang386c2f92023-07-20 15:32:15 +0800260 def cal_libraries_code_size(self) -> typing.Dict[str, str]:
Yanray Wange0e27602023-07-14 17:37:45 +0800261 """Calculate code size of libraries by measurement tool."""
262
Yanray Wang21127f72023-07-19 12:09:45 +0800263 git_worktree_path = self._create_git_worktree()
Yanray Wange0e27602023-07-14 17:37:45 +0800264 self._build_libraries(git_worktree_path)
Yanray Wang21127f72023-07-19 12:09:45 +0800265 res = self._gen_raw_code_size(git_worktree_path)
Yanray Wange0e27602023-07-14 17:37:45 +0800266 self._remove_worktree(git_worktree_path)
267
268 return res
269
270
Yanray Wang15c43f32023-07-17 11:17:12 +0800271class CodeSizeGenerator:
272 """ A generator based on size measurement tool for library objects.
273
274 This is an abstract class. To use it, derive a class that implements
275 size_generator_write_record and size_generator_write_comparison methods,
276 then call both of them with proper arguments.
277 """
Yanray Wang21127f72023-07-19 12:09:45 +0800278 def __init__(self, logger: logging.Logger) -> None:
279 self.logger = logger
280
Yanray Wang15c43f32023-07-17 11:17:12 +0800281 def size_generator_write_record(
282 self,
283 revision: str,
284 code_size_text: typing.Dict,
285 output_file: str
286 ) -> None:
287 """Write size record into a file.
288
289 revision: Git revision.(E.g: commit)
290 code_size_text: text output (utf-8) from code size measurement tool.
291 output_file: file which the code size record is written to.
292 """
293 raise NotImplementedError
294
295 def size_generator_write_comparison(
296 self,
297 old_rev: str,
298 new_rev: str,
Yanray Wang386c2f92023-07-20 15:32:15 +0800299 output_stream: str,
Yanray Wang227576a2023-07-18 14:35:05 +0800300 result_options: SimpleNamespace
Yanray Wang15c43f32023-07-17 11:17:12 +0800301 ) -> None:
302 """Write a comparision result into a stream between two revisions.
303
304 old_rev: old git revision to compared with.
305 new_rev: new git revision to compared with.
306 output_stream: stream which the code size record is written to.
307 (E.g: file / sys.stdout)
Yanray Wang227576a2023-07-18 14:35:05 +0800308 result_options: SimpleNamespace containing options for comparison result.
309 with_markdown: write comparision result in a markdown table. (Default: False)
310 stdout: direct comparison result into sys.stdout. (Default: False)
Yanray Wang15c43f32023-07-17 11:17:12 +0800311 """
312 raise NotImplementedError
313
314
315class CodeSizeGeneratorWithSize(CodeSizeGenerator):
Yanray Wang16ebc572023-05-30 18:10:20 +0800316 """Code Size Base Class for size record saving and writing."""
317
Yanray Wangfc6ed4d2023-07-14 17:33:09 +0800318 class SizeEntry: # pylint: disable=too-few-public-methods
319 """Data Structure to only store information of code size."""
320 def __init__(self, text, data, bss, dec):
321 self.text = text
322 self.data = data
323 self.bss = bss
324 self.total = dec # total <=> dec
325
Yanray Wang21127f72023-07-19 12:09:45 +0800326 def __init__(self, logger: logging.Logger) -> None:
Yanray Wang16ebc572023-05-30 18:10:20 +0800327 """ Variable code_size is used to store size info for any revisions.
328 code_size: (data format)
Yanray Wang9b174e92023-07-17 17:59:53 +0800329 {revision: {module: {file_name: [text, data, bss, dec],
Yanray Wang16ebc572023-05-30 18:10:20 +0800330 etc ...
331 },
332 etc ...
333 },
334 etc ...
335 }
336 """
Yanray Wang21127f72023-07-19 12:09:45 +0800337 super().__init__(logger)
Yanray Wang16ebc572023-05-30 18:10:20 +0800338 self.code_size = {} #type: typing.Dict[str, typing.Dict]
339
Yanray Wang386c2f92023-07-20 15:32:15 +0800340 def _set_size_record(self, revision: str, mod: str, size_text: str) -> None:
Yanray Wang16ebc572023-05-30 18:10:20 +0800341 """Store size information for target revision and high-level module.
342
343 size_text Format: text data bss dec hex filename
344 """
345 size_record = {}
346 for line in size_text.splitlines()[1:]:
347 data = line.split()
Yanray Wang9b174e92023-07-17 17:59:53 +0800348 # file_name: SizeEntry(text, data, bss, dec)
349 size_record[data[5]] = CodeSizeGeneratorWithSize.SizeEntry(
350 data[0], data[1], data[2], data[3])
Yanray Wang16ebc572023-05-30 18:10:20 +0800351 if revision in self.code_size:
352 self.code_size[revision].update({mod: size_record})
353 else:
354 self.code_size[revision] = {mod: size_record}
355
356 def read_size_record(self, revision: str, fname: str) -> None:
357 """Read size information from csv file and write it into code_size.
358
359 fname Format: filename text data bss dec
360 """
361 mod = ""
362 size_record = {}
363 with open(fname, 'r') as csv_file:
364 for line in csv_file:
365 data = line.strip().split()
366 # check if we find the beginning of a module
367 if data and data[0] in MBEDTLS_STATIC_LIB:
368 mod = data[0]
369 continue
370
371 if mod:
Yanray Wang9b174e92023-07-17 17:59:53 +0800372 # file_name: SizeEntry(text, data, bss, dec)
373 size_record[data[0]] = CodeSizeGeneratorWithSize.SizeEntry(
Yanray Wangfc6ed4d2023-07-14 17:33:09 +0800374 data[1], data[2], data[3], data[4])
Yanray Wang16ebc572023-05-30 18:10:20 +0800375
376 # check if we hit record for the end of a module
377 m = re.match(r'.?TOTALS', line)
378 if m:
379 if revision in self.code_size:
380 self.code_size[revision].update({mod: size_record})
381 else:
382 self.code_size[revision] = {mod: size_record}
383 mod = ""
384 size_record = {}
385
386 def _size_reader_helper(
387 self,
388 revision: str,
Yanray Wangb664cb72023-07-18 12:28:35 +0800389 output: typing_util.Writable,
390 with_markdown=False
Yanray Wang16ebc572023-05-30 18:10:20 +0800391 ) -> typing.Iterator[tuple]:
392 """A helper function to peel code_size based on revision."""
393 for mod, file_size in self.code_size[revision].items():
Yanray Wangb664cb72023-07-18 12:28:35 +0800394 if not with_markdown:
395 output.write("\n" + mod + "\n")
Yanray Wang16ebc572023-05-30 18:10:20 +0800396 for fname, size_entry in file_size.items():
397 yield mod, fname, size_entry
398
Yanray Wang386c2f92023-07-20 15:32:15 +0800399 def _write_size_record(
Yanray Wang16ebc572023-05-30 18:10:20 +0800400 self,
401 revision: str,
402 output: typing_util.Writable
403 ) -> None:
404 """Write size information to a file.
405
406 Writing Format: file_name text data bss total(dec)
407 """
Yanray Wangb664cb72023-07-18 12:28:35 +0800408 format_string = "{:<30} {:>7} {:>7} {:>7} {:>7}\n"
409 output.write(format_string.format("filename",
410 "text", "data", "bss", "total"))
Yanray Wang16ebc572023-05-30 18:10:20 +0800411 for _, fname, size_entry in self._size_reader_helper(revision, output):
Yanray Wangb664cb72023-07-18 12:28:35 +0800412 output.write(format_string.format(fname,
413 size_entry.text, size_entry.data,
414 size_entry.bss, size_entry.total))
Yanray Wang16ebc572023-05-30 18:10:20 +0800415
Yanray Wang386c2f92023-07-20 15:32:15 +0800416 def _write_comparison(
Yanray Wang16ebc572023-05-30 18:10:20 +0800417 self,
418 old_rev: str,
419 new_rev: str,
Yanray Wangb664cb72023-07-18 12:28:35 +0800420 output: typing_util.Writable,
421 with_markdown: bool
Yanray Wang16ebc572023-05-30 18:10:20 +0800422 ) -> None:
423 """Write comparison result into a file.
424
Yanray Wang9b174e92023-07-17 17:59:53 +0800425 Writing Format: file_name current(text,data) old(text,data)\
426 change(text,data) change_pct%(text,data)
Yanray Wang16ebc572023-05-30 18:10:20 +0800427 """
Yanray Wang9b174e92023-07-17 17:59:53 +0800428
429 def cal_size_section_variation(mod, fname, size_entry, attr):
430 new_size = int(size_entry.__dict__[attr])
Yanray Wang16ebc572023-05-30 18:10:20 +0800431 # check if we have the file in old revision
432 if fname in self.code_size[old_rev][mod]:
Yanray Wang9b174e92023-07-17 17:59:53 +0800433 old_size = int(self.code_size[old_rev][mod][fname].__dict__[attr])
Yanray Wang16ebc572023-05-30 18:10:20 +0800434 change = new_size - old_size
435 if old_size != 0:
436 change_pct = change / old_size
437 else:
438 change_pct = 0
Yanray Wang9b174e92023-07-17 17:59:53 +0800439 return [new_size, old_size, change, change_pct]
Yanray Wang16ebc572023-05-30 18:10:20 +0800440 else:
Yanray Wang9b174e92023-07-17 17:59:53 +0800441 return [new_size]
442
Yanray Wangb664cb72023-07-18 12:28:35 +0800443 if with_markdown:
444 format_string = "| {:<30} | {:<18} | {:<14} | {:<17} | {:<18} |\n"
445 else:
446 format_string = "{:<30} {:<18} {:<14} {:<17} {:<18}\n"
447
Yanray Wang386c2f92023-07-20 15:32:15 +0800448 output.write(format_string
449 .format("filename",
450 "current(text,data)", "old(text,data)",
451 "change(text,data)", "change%(text,data)"))
Yanray Wangb664cb72023-07-18 12:28:35 +0800452 if with_markdown:
453 output.write(format_string
454 .format("----:", "----:", "----:", "----:", "----:"))
455
Yanray Wang386c2f92023-07-20 15:32:15 +0800456 for mod, fname, size_entry in \
Yanray Wangb664cb72023-07-18 12:28:35 +0800457 self._size_reader_helper(new_rev, output, with_markdown):
458 text_vari = cal_size_section_variation(mod, fname,
459 size_entry, 'text')
460 data_vari = cal_size_section_variation(mod, fname,
461 size_entry, 'data')
Yanray Wang9b174e92023-07-17 17:59:53 +0800462
463 if len(text_vari) != 1:
Yanray Wangb664cb72023-07-18 12:28:35 +0800464 # skip the files that haven't changed in code size if we write
465 # comparison result in a markdown table.
466 if with_markdown and text_vari[2] == 0 and data_vari[2] == 0:
467 continue
Yanray Wang386c2f92023-07-20 15:32:15 +0800468 output.write(
469 format_string
470 .format(fname,
471 str(text_vari[0]) + "," + str(data_vari[0]),
472 str(text_vari[1]) + "," + str(data_vari[1]),
473 str(text_vari[2]) + "," + str(data_vari[2]),
474 "{:.2%}".format(text_vari[3]) + ","
475 + "{:.2%}".format(data_vari[3])))
Yanray Wang9b174e92023-07-17 17:59:53 +0800476 else:
Yanray Wang386c2f92023-07-20 15:32:15 +0800477 output.write("{:<30} {:<18}\n"
478 .format(fname,
479 str(text_vari[0]) + "," + str(data_vari[0])))
Yanray Wang16ebc572023-05-30 18:10:20 +0800480
Yanray Wang15c43f32023-07-17 11:17:12 +0800481 def size_generator_write_record(
482 self,
483 revision: str,
484 code_size_text: typing.Dict,
485 output_file: str
486 ) -> None:
487 """Write size record into a specified file based on Git revision and
488 output from `size` tool."""
Yanray Wang21127f72023-07-19 12:09:45 +0800489 self.logger.debug("Generating code size csv for {}.".format(revision))
490
Yanray Wang15c43f32023-07-17 11:17:12 +0800491 for mod, size_text in code_size_text.items():
Yanray Wang386c2f92023-07-20 15:32:15 +0800492 self._set_size_record(revision, mod, size_text)
Yanray Wang15c43f32023-07-17 11:17:12 +0800493
Yanray Wang15c43f32023-07-17 11:17:12 +0800494 output = open(output_file, "w")
Yanray Wang386c2f92023-07-20 15:32:15 +0800495 self._write_size_record(revision, output)
Yanray Wang15c43f32023-07-17 11:17:12 +0800496
497 def size_generator_write_comparison(
498 self,
499 old_rev: str,
500 new_rev: str,
Yanray Wang386c2f92023-07-20 15:32:15 +0800501 output_stream: str,
Yanray Wang227576a2023-07-18 14:35:05 +0800502 result_options: SimpleNamespace
Yanray Wang15c43f32023-07-17 11:17:12 +0800503 ) -> None:
504 """Write a comparision result into a stream between two revisions."""
Yanray Wang21127f72023-07-19 12:09:45 +0800505 self.logger.debug("Generating comparison results between {} and {}."
506 .format(old_rev, new_rev))
507
Yanray Wang227576a2023-07-18 14:35:05 +0800508 if result_options.stdout:
509 output = sys.stdout
510 else:
511 output = open(output_stream, "w")
Yanray Wang386c2f92023-07-20 15:32:15 +0800512 self._write_comparison(old_rev, new_rev, output,
513 result_options.with_markdown)
Yanray Wang15c43f32023-07-17 11:17:12 +0800514
Yanray Wang16ebc572023-05-30 18:10:20 +0800515
Yanray Wangfc6ed4d2023-07-14 17:33:09 +0800516class CodeSizeComparison:
Xiaofei Bai2400b502021-10-21 12:22:58 +0000517 """Compare code size between two Git revisions."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000518
Yanray Wang72b105f2023-05-31 15:20:39 +0800519 def __init__(
520 self,
Yanray Wang923f9432023-07-17 12:43:00 +0800521 old_size_version: SimpleNamespace,
522 new_size_version: SimpleNamespace,
Yanray Wang802af162023-07-17 14:04:30 +0800523 code_size_common: SimpleNamespace,
Yanray Wang21127f72023-07-19 12:09:45 +0800524 logger: logging.Logger,
Yanray Wang72b105f2023-05-31 15:20:39 +0800525 ) -> None:
Xiaofei Baibca03e52021-09-09 09:42:37 +0000526 """
Yanray Wang6a862582023-05-24 12:24:38 +0800527 old_revision: revision to compare against.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000528 new_revision:
Yanray Wang6a862582023-05-24 12:24:38 +0800529 result_dir: directory for comparison result.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000530 """
Yanray Wang386c2f92023-07-20 15:32:15 +0800531 self.result_dir = os.path.abspath(
532 code_size_common.result_options.result_dir)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000533 os.makedirs(self.result_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000534
535 self.csv_dir = os.path.abspath("code_size_records/")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000536 os.makedirs(self.csv_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000537
Yanray Wang21127f72023-07-19 12:09:45 +0800538 self.logger = logger
539
Yanray Wang923f9432023-07-17 12:43:00 +0800540 self.old_size_version = old_size_version
541 self.new_size_version = new_size_version
Yanray Wang802af162023-07-17 14:04:30 +0800542 self.code_size_common = code_size_common
Yanray Wang386c2f92023-07-20 15:32:15 +0800543 # infer make command
Yanray Wang21127f72023-07-19 12:09:45 +0800544 self.old_size_version.make_cmd = CodeSizeBuildInfo(
545 self.old_size_version, self.code_size_common.host_arch,
546 self.logger).infer_make_command()
547 self.new_size_version.make_cmd = CodeSizeBuildInfo(
548 self.new_size_version, self.code_size_common.host_arch,
549 self.logger).infer_make_command()
Yanray Wang386c2f92023-07-20 15:32:15 +0800550 # initialize size parser with corresponding measurement tool
Yanray Wang21127f72023-07-19 12:09:45 +0800551 self.code_size_generator = self.__generate_size_parser()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000552
Yanray Wang21127f72023-07-19 12:09:45 +0800553 def __generate_size_parser(self):
554 if re.match(r'size', self.code_size_common.measure_cmd.strip()):
555 return CodeSizeGeneratorWithSize(self.logger)
Yanray Wang802af162023-07-17 14:04:30 +0800556 else:
Yanray Wang21127f72023-07-19 12:09:45 +0800557 self.logger.error("Unsupported measurement tool: `{}`."
558 .format(self.code_size_common.measure_cmd
559 .strip().split(' ')[0]))
Yanray Wang802af162023-07-17 14:04:30 +0800560 sys.exit(1)
561
562
Yanray Wang386c2f92023-07-20 15:32:15 +0800563 def cal_code_size(
564 self,
565 size_version: SimpleNamespace
566 ) -> typing.Dict[str, str]:
Yanray Wang5e9130a2023-07-17 11:55:54 +0800567 """Calculate code size of library objects in a UTF-8 encoding"""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000568
Yanray Wang21127f72023-07-19 12:09:45 +0800569 return CodeSizeCalculator(size_version.revision, size_version.make_cmd,
570 self.code_size_common.measure_cmd,
571 self.logger).cal_libraries_code_size()
Yanray Wang8804db92023-05-30 18:18:18 +0800572
Yanray Wang386c2f92023-07-20 15:32:15 +0800573 def gen_file_name(
574 self,
575 old_size_version: SimpleNamespace,
576 new_size_version=None
577 ) -> str:
Yanray Wang21127f72023-07-19 12:09:45 +0800578 """Generate a literal string as csv file name."""
Yanray Wang923f9432023-07-17 12:43:00 +0800579 if new_size_version:
Yanray Wang802af162023-07-17 14:04:30 +0800580 return '{}-{}-{}-{}-{}-{}-{}.csv'\
Yanray Wang386c2f92023-07-20 15:32:15 +0800581 .format(old_size_version.revision, old_size_version.arch,
582 old_size_version.config,
583 new_size_version.revision, new_size_version.arch,
584 new_size_version.config,
585 self.code_size_common.measure_cmd.strip()\
586 .split(' ')[0])
Yanray Wang923f9432023-07-17 12:43:00 +0800587 else:
Yanray Wang802af162023-07-17 14:04:30 +0800588 return '{}-{}-{}-{}.csv'\
Yanray Wang386c2f92023-07-20 15:32:15 +0800589 .format(old_size_version.revision, old_size_version.arch,
590 old_size_version.config,
591 self.code_size_common.measure_cmd.strip()\
592 .split(' ')[0])
Yanray Wang923f9432023-07-17 12:43:00 +0800593
Yanray Wang386c2f92023-07-20 15:32:15 +0800594 def gen_code_size_report(self, size_version: SimpleNamespace) -> None:
Yanray Wang5e9130a2023-07-17 11:55:54 +0800595 """Generate code size record and write it into a file."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000596
Yanray Wang21127f72023-07-19 12:09:45 +0800597 self.logger.info("Start to generate code size record for {}."
598 .format(size_version.revision))
599 output_file = os.path.join(self.csv_dir,
600 self.gen_file_name(size_version))
Xiaofei Baibca03e52021-09-09 09:42:37 +0000601 # Check if the corresponding record exists
Yanray Wang21127f72023-07-19 12:09:45 +0800602 if size_version.revision != "current" and \
603 os.path.exists(output_file):
604 self.logger.debug("Code size csv file for {} already exists."
605 .format(size_version.revision))
606 self.code_size_generator.read_size_record(
607 size_version.revision, output_file)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000608 else:
Yanray Wang386c2f92023-07-20 15:32:15 +0800609 self.code_size_generator.size_generator_write_record(
610 size_version.revision, self.cal_code_size(size_version),
611 output_file)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000612
Yanray Wang386c2f92023-07-20 15:32:15 +0800613 def gen_code_size_comparison(self) -> None:
Yanray Wang5e9130a2023-07-17 11:55:54 +0800614 """Generate results of code size changes between two revisions,
Xiaofei Baibca03e52021-09-09 09:42:37 +0000615 old and new. Measured code size results of these two revisions
Xiaofei Bai2400b502021-10-21 12:22:58 +0000616 must be available."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000617
Yanray Wang21127f72023-07-19 12:09:45 +0800618 self.logger.info("Start to generate comparision result between "\
619 "{} and {}."
620 .format(self.old_size_version.revision,
621 self.new_size_version.revision))
622 output_file = os.path.join(
623 self.result_dir,
624 self.gen_file_name(self.old_size_version, self.new_size_version))
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000625
Yanray Wang21127f72023-07-19 12:09:45 +0800626 self.code_size_generator.size_generator_write_comparison(
627 self.old_size_version.revision, self.new_size_version.revision,
628 output_file, self.code_size_common.result_options)
629
Yanray Wang386c2f92023-07-20 15:32:15 +0800630 def get_comparision_results(self) -> None:
Xiaofei Baibca03e52021-09-09 09:42:37 +0000631 """Compare size of library/*.o between self.old_rev and self.new_rev,
632 and generate the result file."""
Gilles Peskined9071e72022-09-18 21:17:09 +0200633 build_tree.check_repo_path()
Yanray Wang923f9432023-07-17 12:43:00 +0800634 self.gen_code_size_report(self.old_size_version)
635 self.gen_code_size_report(self.new_size_version)
Yanray Wang386c2f92023-07-20 15:32:15 +0800636 self.gen_code_size_comparison()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000637
Yanray Wang923f9432023-07-17 12:43:00 +0800638
Xiaofei Bai2400b502021-10-21 12:22:58 +0000639def main():
Yanray Wang502c54f2023-05-31 11:41:36 +0800640 parser = argparse.ArgumentParser(description=(__doc__))
641 group_required = parser.add_argument_group(
642 'required arguments',
643 'required arguments to parse for running ' + os.path.basename(__file__))
644 group_required.add_argument(
645 "-o", "--old-rev", type=str, required=True,
646 help="old revision for comparison.")
647
648 group_optional = parser.add_argument_group(
649 'optional arguments',
650 'optional arguments to parse for running ' + os.path.basename(__file__))
651 group_optional.add_argument(
Xiaofei Baibca03e52021-09-09 09:42:37 +0000652 "-r", "--result-dir", type=str, default="comparison",
653 help="directory where comparison result is stored, \
Yanray Wang502c54f2023-05-31 11:41:36 +0800654 default is comparison")
655 group_optional.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000656 "-n", "--new-rev", type=str, default=None,
657 help="new revision for comparison, default is the current work \
Yanray Wang502c54f2023-05-31 11:41:36 +0800658 directory, including uncommitted changes.")
659 group_optional.add_argument(
Yanray Wang23bd5322023-05-24 11:03:59 +0800660 "-a", "--arch", type=str, default=detect_arch(),
661 choices=list(map(lambda s: s.value, SupportedArch)),
662 help="specify architecture for code size comparison, default is the\
Yanray Wang502c54f2023-05-31 11:41:36 +0800663 host architecture.")
664 group_optional.add_argument(
Yanray Wang6a862582023-05-24 12:24:38 +0800665 "-c", "--config", type=str, default=SupportedConfig.DEFAULT.value,
666 choices=list(map(lambda s: s.value, SupportedConfig)),
667 help="specify configuration type for code size comparison,\
Yanray Wang502c54f2023-05-31 11:41:36 +0800668 default is the current MbedTLS configuration.")
Yanray Wangb664cb72023-07-18 12:28:35 +0800669 group_optional.add_argument(
670 '--markdown', action='store_true', dest='markdown',
671 help="Show comparision of code size in a markdown table\
672 (only show the files that have changed).")
Yanray Wang227576a2023-07-18 14:35:05 +0800673 group_optional.add_argument(
674 '--stdout', action='store_true', dest='stdout',
675 help="Set this option to direct comparison result into sys.stdout.\
676 (Default: file)")
Yanray Wang21127f72023-07-19 12:09:45 +0800677 group_optional.add_argument(
678 '--verbose', action='store_true', dest='verbose',
679 help="Show logs in detail for code size measurement. (Default: False)")
Xiaofei Baibca03e52021-09-09 09:42:37 +0000680 comp_args = parser.parse_args()
681
Yanray Wang21127f72023-07-19 12:09:45 +0800682 logger = logging.getLogger()
683 logging_util.configure_logger(logger)
684 logger.setLevel(logging.DEBUG if comp_args.verbose else logging.INFO)
685
Xiaofei Baibca03e52021-09-09 09:42:37 +0000686 if os.path.isfile(comp_args.result_dir):
Yanray Wang21127f72023-07-19 12:09:45 +0800687 logger.error("{} is not a directory".format(comp_args.result_dir))
Xiaofei Baibca03e52021-09-09 09:42:37 +0000688 parser.exit()
689
Yanray Wang386c2f92023-07-20 15:32:15 +0800690 old_revision = CodeSizeCalculator.validate_revision(comp_args.old_rev)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000691 if comp_args.new_rev is not None:
Yanray Wang386c2f92023-07-20 15:32:15 +0800692 new_revision = CodeSizeCalculator.validate_revision(comp_args.new_rev)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000693 else:
694 new_revision = "current"
Xiaofei Bai2400b502021-10-21 12:22:58 +0000695
Yanray Wang923f9432023-07-17 12:43:00 +0800696 old_size_version = SimpleNamespace(
Yanray Wang386c2f92023-07-20 15:32:15 +0800697 version='old',
Yanray Wang923f9432023-07-17 12:43:00 +0800698 revision=old_revision,
699 config=comp_args.config,
700 arch=comp_args.arch,
Yanray Wang923f9432023-07-17 12:43:00 +0800701 make_cmd='',
702 )
703 new_size_version = SimpleNamespace(
Yanray Wang386c2f92023-07-20 15:32:15 +0800704 version='new',
Yanray Wang923f9432023-07-17 12:43:00 +0800705 revision=new_revision,
706 config=comp_args.config,
707 arch=comp_args.arch,
Yanray Wang923f9432023-07-17 12:43:00 +0800708 make_cmd='',
709 )
Yanray Wang802af162023-07-17 14:04:30 +0800710 code_size_common = SimpleNamespace(
Yanray Wang227576a2023-07-18 14:35:05 +0800711 result_options=SimpleNamespace(
712 result_dir=comp_args.result_dir,
713 with_markdown=comp_args.markdown,
714 stdout=comp_args.stdout,
715 ),
Yanray Wang802af162023-07-17 14:04:30 +0800716 host_arch=detect_arch(),
717 measure_cmd='size -t',
718 )
Yanray Wang923f9432023-07-17 12:43:00 +0800719
Yanray Wang21127f72023-07-19 12:09:45 +0800720 logger.info("Measure code size between {}:{}-{} and {}:{}-{} by `{}`."
721 .format(old_size_version.revision, old_size_version.config,
722 old_size_version.arch,
723 new_size_version.revision, old_size_version.config,
724 new_size_version.arch,
725 code_size_common.measure_cmd.strip().split(' ')[0]))
Yanray Wang386c2f92023-07-20 15:32:15 +0800726 CodeSizeComparison(old_size_version, new_size_version,
727 code_size_common, logger).get_comparision_results()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000728
Xiaofei Baibca03e52021-09-09 09:42:37 +0000729if __name__ == "__main__":
Xiaofei Bai2400b502021-10-21 12:22:58 +0000730 main()