blob: dc41d262d5ae536f9326d59a0abcce7fd2cbe78b [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 Wang6a862582023-05-24 12:24:38 +080048CONFIG_TFM_MEDIUM_MBEDCRYPTO_H = "../configs/tfm_mbedcrypto_config_profile_medium.h"
49CONFIG_TFM_MEDIUM_PSA_CRYPTO_H = "../configs/crypto_config_profile_medium.h"
50class 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()
66 if "__aarch64__" in cc_output:
67 return SupportedArch.AARCH64.value
68 if "__arm__" in cc_output:
69 return SupportedArch.AARCH32.value
70 if "__x86_64__" in cc_output:
71 return SupportedArch.X86_64.value
72 if "__x86__" in cc_output:
73 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 = [
86 "-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,
91 ]
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 Wang6a862582023-05-24 12:24:38 +0800110 """Infer build command based on architecture and configuration."""
111
Yanray Wang923f9432023-07-17 12:43:00 +0800112 if self.size_version.config == SupportedConfig.DEFAULT.value and \
Yanray Wang802af162023-07-17 14:04:30 +0800113 self.size_version.arch == self.host_arch:
Yanray Wang6a862582023-05-24 12:24:38 +0800114 return 'make -j lib CFLAGS=\'-Os \' '
Yanray Wang923f9432023-07-17 12:43:00 +0800115 elif self.size_version.arch == SupportedArch.ARMV8_M.value and \
116 self.size_version.config == SupportedConfig.TFM_MEDIUM.value:
Yanray Wang6a862582023-05-24 12:24:38 +0800117 return \
Yanray Wang60430bd2023-05-29 14:48:18 +0800118 'make -j lib CC=armclang \
Yanray Wang6a862582023-05-24 12:24:38 +0800119 CFLAGS=\'--target=arm-arm-none-eabi -mcpu=cortex-m33 -Os \
120 -DMBEDTLS_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_MBEDCRYPTO_H + '\\\" \
121 -DMBEDTLS_PSA_CRYPTO_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_PSA_CRYPTO_H + '\\\" \''
122 else:
Yanray Wang21127f72023-07-19 12:09:45 +0800123 self.logger.error("Unsupported combination of architecture: {} " \
124 "and configuration: {}.\n"
125 .format(self.size_version.arch,
126 self.size_version.config))
127 self.logger.info("Please use supported combination of " \
128 "architecture and configuration:")
Yanray Wang923f9432023-07-17 12:43:00 +0800129 for comb in CodeSizeBuildInfo.SupportedArchConfig:
Yanray Wang21127f72023-07-19 12:09:45 +0800130 self.logger.info(comb)
131 self.logger.info("")
132 self.logger.info("For your system, please use:")
Yanray Wang923f9432023-07-17 12:43:00 +0800133 for comb in CodeSizeBuildInfo.SupportedArchConfig:
Yanray Wang802af162023-07-17 14:04:30 +0800134 if "default" in comb and self.host_arch not in comb:
Yanray Wang21f17442023-06-01 11:29:06 +0800135 continue
Yanray Wang21127f72023-07-19 12:09:45 +0800136 self.logger.info(comb)
Yanray Wang6a862582023-05-24 12:24:38 +0800137 sys.exit(1)
138
139
Yanray Wange0e27602023-07-14 17:37:45 +0800140class CodeSizeCalculator:
141 """ A calculator to calculate code size of library objects based on
142 Git revision and code size measurement tool.
143 """
144
145 def __init__(
146 self,
147 revision: str,
148 make_cmd: str,
Yanray Wang21127f72023-07-19 12:09:45 +0800149 measure_cmd: str,
150 logger: logging.Logger,
Yanray Wange0e27602023-07-14 17:37:45 +0800151 ) -> None:
152 """
153 revision: Git revision.(E.g: commit)
Yanray Wang802af162023-07-17 14:04:30 +0800154 make_cmd: command to build objects in library.
155 measure_cmd: command to measure code size for objects in library.
Yanray Wange0e27602023-07-14 17:37:45 +0800156 """
157 self.repo_path = "."
158 self.git_command = "git"
159 self.make_clean = 'make clean'
160
161 self.revision = revision
162 self.make_cmd = make_cmd
Yanray Wang802af162023-07-17 14:04:30 +0800163 self.measure_cmd = measure_cmd
Yanray Wang21127f72023-07-19 12:09:45 +0800164 self.logger = logger
Yanray Wange0e27602023-07-14 17:37:45 +0800165
166 @staticmethod
167 def validate_revision(revision: str) -> bytes:
168 result = subprocess.check_output(["git", "rev-parse", "--verify",
169 revision + "^{commit}"], shell=False)
170 return result
171
Yanray Wang21127f72023-07-19 12:09:45 +0800172 def _create_git_worktree(self) -> str:
Yanray Wange0e27602023-07-14 17:37:45 +0800173 """Make a separate worktree for revision.
174 Do not modify the current worktree."""
175
Yanray Wang21127f72023-07-19 12:09:45 +0800176 if self.revision == "current":
177 self.logger.debug("Using current work directory.")
Yanray Wange0e27602023-07-14 17:37:45 +0800178 git_worktree_path = self.repo_path
179 else:
Yanray Wang21127f72023-07-19 12:09:45 +0800180 self.logger.debug("Creating git worktree for {}."
181 .format(self.revision))
182 git_worktree_path = os.path.join(self.repo_path,
183 "temp-" + self.revision)
Yanray Wange0e27602023-07-14 17:37:45 +0800184 subprocess.check_output(
185 [self.git_command, "worktree", "add", "--detach",
Yanray Wang21127f72023-07-19 12:09:45 +0800186 git_worktree_path, self.revision], cwd=self.repo_path,
Yanray Wange0e27602023-07-14 17:37:45 +0800187 stderr=subprocess.STDOUT
188 )
189
190 return git_worktree_path
191
192 def _build_libraries(self, git_worktree_path: str) -> None:
193 """Build libraries in the specified worktree."""
194
Yanray Wang21127f72023-07-19 12:09:45 +0800195 self.logger.debug("Building objects of library for {}."
196 .format(self.revision))
Yanray Wange0e27602023-07-14 17:37:45 +0800197 my_environment = os.environ.copy()
198 try:
199 subprocess.check_output(
200 self.make_clean, env=my_environment, shell=True,
201 cwd=git_worktree_path, stderr=subprocess.STDOUT,
202 )
203 subprocess.check_output(
204 self.make_cmd, env=my_environment, shell=True,
205 cwd=git_worktree_path, stderr=subprocess.STDOUT,
206 )
207 except subprocess.CalledProcessError as e:
208 self._handle_called_process_error(e, git_worktree_path)
209
Yanray Wang21127f72023-07-19 12:09:45 +0800210 def _gen_raw_code_size(self, git_worktree_path: str) -> typing.Dict:
Yanray Wange0e27602023-07-14 17:37:45 +0800211 """Calculate code size with measurement tool in UTF-8 encoding."""
Yanray Wang21127f72023-07-19 12:09:45 +0800212
213 self.logger.debug("Measuring code size for {} by `{}`."
214 .format(self.revision,
215 self.measure_cmd.strip().split(' ')[0]))
Yanray Wange0e27602023-07-14 17:37:45 +0800216
217 res = {}
218 for mod, st_lib in MBEDTLS_STATIC_LIB.items():
219 try:
220 result = subprocess.check_output(
Yanray Wang802af162023-07-17 14:04:30 +0800221 [self.measure_cmd + ' ' + st_lib], cwd=git_worktree_path,
222 shell=True, universal_newlines=True
Yanray Wange0e27602023-07-14 17:37:45 +0800223 )
224 res[mod] = result
225 except subprocess.CalledProcessError as e:
226 self._handle_called_process_error(e, git_worktree_path)
227
228 return res
229
230 def _remove_worktree(self, git_worktree_path: str) -> None:
231 """Remove temporary worktree."""
232 if git_worktree_path != self.repo_path:
Yanray Wang21127f72023-07-19 12:09:45 +0800233 self.logger.debug("Removing temporary worktree {}."
234 .format(git_worktree_path))
Yanray Wange0e27602023-07-14 17:37:45 +0800235 subprocess.check_output(
236 [self.git_command, "worktree", "remove", "--force",
237 git_worktree_path], cwd=self.repo_path,
238 stderr=subprocess.STDOUT
239 )
240
241 def _handle_called_process_error(self, e: subprocess.CalledProcessError,
242 git_worktree_path: str) -> None:
243 """Handle a CalledProcessError and quit the program gracefully.
244 Remove any extra worktrees so that the script may be called again."""
245
246 # Tell the user what went wrong
Yanray Wang21127f72023-07-19 12:09:45 +0800247 self.logger.error(e, exc_info=True)
248 self.logger.error("Process output:\n {}".format(str(e.output, "utf-8")))
Yanray Wange0e27602023-07-14 17:37:45 +0800249
250 # Quit gracefully by removing the existing worktree
251 self._remove_worktree(git_worktree_path)
252 sys.exit(-1)
253
254 def cal_libraries_code_size(self) -> typing.Dict:
255 """Calculate code size of libraries by measurement tool."""
256
Yanray Wang21127f72023-07-19 12:09:45 +0800257 git_worktree_path = self._create_git_worktree()
Yanray Wange0e27602023-07-14 17:37:45 +0800258 self._build_libraries(git_worktree_path)
Yanray Wang21127f72023-07-19 12:09:45 +0800259 res = self._gen_raw_code_size(git_worktree_path)
Yanray Wange0e27602023-07-14 17:37:45 +0800260 self._remove_worktree(git_worktree_path)
261
262 return res
263
264
Yanray Wang15c43f32023-07-17 11:17:12 +0800265class CodeSizeGenerator:
266 """ A generator based on size measurement tool for library objects.
267
268 This is an abstract class. To use it, derive a class that implements
269 size_generator_write_record and size_generator_write_comparison methods,
270 then call both of them with proper arguments.
271 """
Yanray Wang21127f72023-07-19 12:09:45 +0800272 def __init__(self, logger: logging.Logger) -> None:
273 self.logger = logger
274
Yanray Wang15c43f32023-07-17 11:17:12 +0800275 def size_generator_write_record(
276 self,
277 revision: str,
278 code_size_text: typing.Dict,
279 output_file: str
280 ) -> None:
281 """Write size record into a file.
282
283 revision: Git revision.(E.g: commit)
284 code_size_text: text output (utf-8) from code size measurement tool.
285 output_file: file which the code size record is written to.
286 """
287 raise NotImplementedError
288
289 def size_generator_write_comparison(
290 self,
291 old_rev: str,
292 new_rev: str,
Yanray Wangb664cb72023-07-18 12:28:35 +0800293 output_stream,
Yanray Wang227576a2023-07-18 14:35:05 +0800294 result_options: SimpleNamespace
Yanray Wang15c43f32023-07-17 11:17:12 +0800295 ) -> None:
296 """Write a comparision result into a stream between two revisions.
297
298 old_rev: old git revision to compared with.
299 new_rev: new git revision to compared with.
300 output_stream: stream which the code size record is written to.
301 (E.g: file / sys.stdout)
Yanray Wang227576a2023-07-18 14:35:05 +0800302 result_options: SimpleNamespace containing options for comparison result.
303 with_markdown: write comparision result in a markdown table. (Default: False)
304 stdout: direct comparison result into sys.stdout. (Default: False)
Yanray Wang15c43f32023-07-17 11:17:12 +0800305 """
306 raise NotImplementedError
307
308
309class CodeSizeGeneratorWithSize(CodeSizeGenerator):
Yanray Wang16ebc572023-05-30 18:10:20 +0800310 """Code Size Base Class for size record saving and writing."""
311
Yanray Wangfc6ed4d2023-07-14 17:33:09 +0800312 class SizeEntry: # pylint: disable=too-few-public-methods
313 """Data Structure to only store information of code size."""
314 def __init__(self, text, data, bss, dec):
315 self.text = text
316 self.data = data
317 self.bss = bss
318 self.total = dec # total <=> dec
319
Yanray Wang21127f72023-07-19 12:09:45 +0800320 def __init__(self, logger: logging.Logger) -> None:
Yanray Wang16ebc572023-05-30 18:10:20 +0800321 """ Variable code_size is used to store size info for any revisions.
322 code_size: (data format)
Yanray Wang9b174e92023-07-17 17:59:53 +0800323 {revision: {module: {file_name: [text, data, bss, dec],
Yanray Wang16ebc572023-05-30 18:10:20 +0800324 etc ...
325 },
326 etc ...
327 },
328 etc ...
329 }
330 """
Yanray Wang21127f72023-07-19 12:09:45 +0800331 super().__init__(logger)
Yanray Wang16ebc572023-05-30 18:10:20 +0800332 self.code_size = {} #type: typing.Dict[str, typing.Dict]
333
334 def set_size_record(self, revision: str, mod: str, size_text: str) -> None:
335 """Store size information for target revision and high-level module.
336
337 size_text Format: text data bss dec hex filename
338 """
339 size_record = {}
340 for line in size_text.splitlines()[1:]:
341 data = line.split()
Yanray Wang9b174e92023-07-17 17:59:53 +0800342 # file_name: SizeEntry(text, data, bss, dec)
343 size_record[data[5]] = CodeSizeGeneratorWithSize.SizeEntry(
344 data[0], data[1], data[2], data[3])
Yanray Wang16ebc572023-05-30 18:10:20 +0800345 if revision in self.code_size:
346 self.code_size[revision].update({mod: size_record})
347 else:
348 self.code_size[revision] = {mod: size_record}
349
350 def read_size_record(self, revision: str, fname: str) -> None:
351 """Read size information from csv file and write it into code_size.
352
353 fname Format: filename text data bss dec
354 """
355 mod = ""
356 size_record = {}
357 with open(fname, 'r') as csv_file:
358 for line in csv_file:
359 data = line.strip().split()
360 # check if we find the beginning of a module
361 if data and data[0] in MBEDTLS_STATIC_LIB:
362 mod = data[0]
363 continue
364
365 if mod:
Yanray Wang9b174e92023-07-17 17:59:53 +0800366 # file_name: SizeEntry(text, data, bss, dec)
367 size_record[data[0]] = CodeSizeGeneratorWithSize.SizeEntry(
Yanray Wangfc6ed4d2023-07-14 17:33:09 +0800368 data[1], data[2], data[3], data[4])
Yanray Wang16ebc572023-05-30 18:10:20 +0800369
370 # check if we hit record for the end of a module
371 m = re.match(r'.?TOTALS', line)
372 if m:
373 if revision in self.code_size:
374 self.code_size[revision].update({mod: size_record})
375 else:
376 self.code_size[revision] = {mod: size_record}
377 mod = ""
378 size_record = {}
379
380 def _size_reader_helper(
381 self,
382 revision: str,
Yanray Wangb664cb72023-07-18 12:28:35 +0800383 output: typing_util.Writable,
384 with_markdown=False
Yanray Wang16ebc572023-05-30 18:10:20 +0800385 ) -> typing.Iterator[tuple]:
386 """A helper function to peel code_size based on revision."""
387 for mod, file_size in self.code_size[revision].items():
Yanray Wangb664cb72023-07-18 12:28:35 +0800388 if not with_markdown:
389 output.write("\n" + mod + "\n")
Yanray Wang16ebc572023-05-30 18:10:20 +0800390 for fname, size_entry in file_size.items():
391 yield mod, fname, size_entry
392
393 def write_size_record(
394 self,
395 revision: str,
396 output: typing_util.Writable
397 ) -> None:
398 """Write size information to a file.
399
400 Writing Format: file_name text data bss total(dec)
401 """
Yanray Wangb664cb72023-07-18 12:28:35 +0800402 format_string = "{:<30} {:>7} {:>7} {:>7} {:>7}\n"
403 output.write(format_string.format("filename",
404 "text", "data", "bss", "total"))
Yanray Wang16ebc572023-05-30 18:10:20 +0800405 for _, fname, size_entry in self._size_reader_helper(revision, output):
Yanray Wangb664cb72023-07-18 12:28:35 +0800406 output.write(format_string.format(fname,
407 size_entry.text, size_entry.data,
408 size_entry.bss, size_entry.total))
Yanray Wang16ebc572023-05-30 18:10:20 +0800409
410 def write_comparison(
411 self,
412 old_rev: str,
413 new_rev: str,
Yanray Wangb664cb72023-07-18 12:28:35 +0800414 output: typing_util.Writable,
415 with_markdown: bool
Yanray Wang16ebc572023-05-30 18:10:20 +0800416 ) -> None:
417 """Write comparison result into a file.
418
Yanray Wang9b174e92023-07-17 17:59:53 +0800419 Writing Format: file_name current(text,data) old(text,data)\
420 change(text,data) change_pct%(text,data)
Yanray Wang16ebc572023-05-30 18:10:20 +0800421 """
Yanray Wang9b174e92023-07-17 17:59:53 +0800422
423 def cal_size_section_variation(mod, fname, size_entry, attr):
424 new_size = int(size_entry.__dict__[attr])
Yanray Wang16ebc572023-05-30 18:10:20 +0800425 # check if we have the file in old revision
426 if fname in self.code_size[old_rev][mod]:
Yanray Wang9b174e92023-07-17 17:59:53 +0800427 old_size = int(self.code_size[old_rev][mod][fname].__dict__[attr])
Yanray Wang16ebc572023-05-30 18:10:20 +0800428 change = new_size - old_size
429 if old_size != 0:
430 change_pct = change / old_size
431 else:
432 change_pct = 0
Yanray Wang9b174e92023-07-17 17:59:53 +0800433 return [new_size, old_size, change, change_pct]
Yanray Wang16ebc572023-05-30 18:10:20 +0800434 else:
Yanray Wang9b174e92023-07-17 17:59:53 +0800435 return [new_size]
436
Yanray Wangb664cb72023-07-18 12:28:35 +0800437 if with_markdown:
438 format_string = "| {:<30} | {:<18} | {:<14} | {:<17} | {:<18} |\n"
439 else:
440 format_string = "{:<30} {:<18} {:<14} {:<17} {:<18}\n"
441
442 output.write(format_string.format("filename", "current(text,data)",\
443 "old(text,data)", "change(text,data)", "change%(text,data)"))
444 if with_markdown:
445 output.write(format_string
446 .format("----:", "----:", "----:", "----:", "----:"))
447
448 for mod, fname, size_entry in\
449 self._size_reader_helper(new_rev, output, with_markdown):
450 text_vari = cal_size_section_variation(mod, fname,
451 size_entry, 'text')
452 data_vari = cal_size_section_variation(mod, fname,
453 size_entry, 'data')
Yanray Wang9b174e92023-07-17 17:59:53 +0800454
455 if len(text_vari) != 1:
Yanray Wangb664cb72023-07-18 12:28:35 +0800456 # skip the files that haven't changed in code size if we write
457 # comparison result in a markdown table.
458 if with_markdown and text_vari[2] == 0 and data_vari[2] == 0:
459 continue
460 output.write(format_string.format(fname,\
461 str(text_vari[0]) + "," + str(data_vari[0]),\
462 str(text_vari[1]) + "," + str(data_vari[1]),\
463 str(text_vari[2]) + "," + str(data_vari[2]),\
464 "{:.2%}".format(text_vari[3]) + "," +\
465 "{:.2%}".format(data_vari[3])))
Yanray Wang9b174e92023-07-17 17:59:53 +0800466 else:
Yanray Wangb664cb72023-07-18 12:28:35 +0800467 output.write("{:<30} {:<18}\n".format(fname,\
468 str(text_vari[0]) + "," + str(data_vari[0])))
Yanray Wang16ebc572023-05-30 18:10:20 +0800469
Yanray Wang15c43f32023-07-17 11:17:12 +0800470 def size_generator_write_record(
471 self,
472 revision: str,
473 code_size_text: typing.Dict,
474 output_file: str
475 ) -> None:
476 """Write size record into a specified file based on Git revision and
477 output from `size` tool."""
Yanray Wang21127f72023-07-19 12:09:45 +0800478 self.logger.debug("Generating code size csv for {}.".format(revision))
479
Yanray Wang15c43f32023-07-17 11:17:12 +0800480 for mod, size_text in code_size_text.items():
481 self.set_size_record(revision, mod, size_text)
482
Yanray Wang15c43f32023-07-17 11:17:12 +0800483 output = open(output_file, "w")
484 self.write_size_record(revision, output)
485
486 def size_generator_write_comparison(
487 self,
488 old_rev: str,
489 new_rev: str,
Yanray Wangb664cb72023-07-18 12:28:35 +0800490 output_stream,
Yanray Wang227576a2023-07-18 14:35:05 +0800491 result_options: SimpleNamespace
Yanray Wang15c43f32023-07-17 11:17:12 +0800492 ) -> None:
493 """Write a comparision result into a stream between two revisions."""
Yanray Wang21127f72023-07-19 12:09:45 +0800494 self.logger.debug("Generating comparison results between {} and {}."
495 .format(old_rev, new_rev))
496
Yanray Wang227576a2023-07-18 14:35:05 +0800497 if result_options.stdout:
498 output = sys.stdout
499 else:
500 output = open(output_stream, "w")
501 self.write_comparison(old_rev, new_rev, output, result_options.with_markdown)
Yanray Wang15c43f32023-07-17 11:17:12 +0800502
Yanray Wang16ebc572023-05-30 18:10:20 +0800503
Yanray Wangfc6ed4d2023-07-14 17:33:09 +0800504class CodeSizeComparison:
Xiaofei Bai2400b502021-10-21 12:22:58 +0000505 """Compare code size between two Git revisions."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000506
Yanray Wang72b105f2023-05-31 15:20:39 +0800507 def __init__(
508 self,
Yanray Wang923f9432023-07-17 12:43:00 +0800509 old_size_version: SimpleNamespace,
510 new_size_version: SimpleNamespace,
Yanray Wang802af162023-07-17 14:04:30 +0800511 code_size_common: SimpleNamespace,
Yanray Wang21127f72023-07-19 12:09:45 +0800512 logger: logging.Logger,
Yanray Wang72b105f2023-05-31 15:20:39 +0800513 ) -> None:
Xiaofei Baibca03e52021-09-09 09:42:37 +0000514 """
Yanray Wang6a862582023-05-24 12:24:38 +0800515 old_revision: revision to compare against.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000516 new_revision:
Yanray Wang6a862582023-05-24 12:24:38 +0800517 result_dir: directory for comparison result.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000518 """
519 self.repo_path = "."
Yanray Wang227576a2023-07-18 14:35:05 +0800520 self.result_dir = os.path.abspath(code_size_common.result_options.result_dir)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000521 os.makedirs(self.result_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000522
523 self.csv_dir = os.path.abspath("code_size_records/")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000524 os.makedirs(self.csv_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000525
Yanray Wang21127f72023-07-19 12:09:45 +0800526 self.logger = logger
527
Yanray Wang923f9432023-07-17 12:43:00 +0800528 self.old_size_version = old_size_version
529 self.new_size_version = new_size_version
Yanray Wang802af162023-07-17 14:04:30 +0800530 self.code_size_common = code_size_common
Yanray Wang21127f72023-07-19 12:09:45 +0800531 self.old_size_version.make_cmd = CodeSizeBuildInfo(
532 self.old_size_version, self.code_size_common.host_arch,
533 self.logger).infer_make_command()
534 self.new_size_version.make_cmd = CodeSizeBuildInfo(
535 self.new_size_version, self.code_size_common.host_arch,
536 self.logger).infer_make_command()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000537 self.git_command = "git"
Yanray Wang4c26db02023-07-04 16:49:04 +0800538 self.make_clean = 'make clean'
Yanray Wang21127f72023-07-19 12:09:45 +0800539 self.code_size_generator = self.__generate_size_parser()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000540
Yanray Wang21127f72023-07-19 12:09:45 +0800541 def __generate_size_parser(self):
542 if re.match(r'size', self.code_size_common.measure_cmd.strip()):
543 return CodeSizeGeneratorWithSize(self.logger)
Yanray Wang802af162023-07-17 14:04:30 +0800544 else:
Yanray Wang21127f72023-07-19 12:09:45 +0800545 self.logger.error("Unsupported measurement tool: `{}`."
546 .format(self.code_size_common.measure_cmd
547 .strip().split(' ')[0]))
Yanray Wang802af162023-07-17 14:04:30 +0800548 sys.exit(1)
549
550
551 def cal_code_size(self, size_version: SimpleNamespace):
Yanray Wang5e9130a2023-07-17 11:55:54 +0800552 """Calculate code size of library objects in a UTF-8 encoding"""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000553
Yanray Wang21127f72023-07-19 12:09:45 +0800554 return CodeSizeCalculator(size_version.revision, size_version.make_cmd,
555 self.code_size_common.measure_cmd,
556 self.logger).cal_libraries_code_size()
Yanray Wang8804db92023-05-30 18:18:18 +0800557
Yanray Wang802af162023-07-17 14:04:30 +0800558 def gen_file_name(self, old_size_version, new_size_version=None):
Yanray Wang21127f72023-07-19 12:09:45 +0800559 """Generate a literal string as csv file name."""
Yanray Wang923f9432023-07-17 12:43:00 +0800560 if new_size_version:
Yanray Wang802af162023-07-17 14:04:30 +0800561 return '{}-{}-{}-{}-{}-{}-{}.csv'\
Yanray Wang923f9432023-07-17 12:43:00 +0800562 .format(old_size_version.revision[:7],
563 old_size_version.arch, old_size_version.config,
564 new_size_version.revision[:7],
Yanray Wang802af162023-07-17 14:04:30 +0800565 new_size_version.arch, new_size_version.config,
566 self.code_size_common.measure_cmd.strip().split(' ')[0])
Yanray Wang923f9432023-07-17 12:43:00 +0800567 else:
Yanray Wang802af162023-07-17 14:04:30 +0800568 return '{}-{}-{}-{}.csv'\
Yanray Wang923f9432023-07-17 12:43:00 +0800569 .format(old_size_version.revision[:7],
Yanray Wang802af162023-07-17 14:04:30 +0800570 old_size_version.arch, old_size_version.config,
571 self.code_size_common.measure_cmd.strip().split(' ')[0])
Yanray Wang923f9432023-07-17 12:43:00 +0800572
573 def gen_code_size_report(self, size_version: SimpleNamespace):
Yanray Wang5e9130a2023-07-17 11:55:54 +0800574 """Generate code size record and write it into a file."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000575
Yanray Wang21127f72023-07-19 12:09:45 +0800576 self.logger.info("Start to generate code size record for {}."
577 .format(size_version.revision))
578 output_file = os.path.join(self.csv_dir,
579 self.gen_file_name(size_version))
Xiaofei Baibca03e52021-09-09 09:42:37 +0000580 # Check if the corresponding record exists
Yanray Wang21127f72023-07-19 12:09:45 +0800581 if size_version.revision != "current" and \
582 os.path.exists(output_file):
583 self.logger.debug("Code size csv file for {} already exists."
584 .format(size_version.revision))
585 self.code_size_generator.read_size_record(
586 size_version.revision, output_file)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000587 else:
Yanray Wang923f9432023-07-17 12:43:00 +0800588 self.code_size_generator.size_generator_write_record(\
589 size_version.revision, self.cal_code_size(size_version),
590 output_file)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000591
Yanray Wang5e9130a2023-07-17 11:55:54 +0800592 def gen_code_size_comparison(self) -> int:
593 """Generate results of code size changes between two revisions,
Xiaofei Baibca03e52021-09-09 09:42:37 +0000594 old and new. Measured code size results of these two revisions
Xiaofei Bai2400b502021-10-21 12:22:58 +0000595 must be available."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000596
Yanray Wang21127f72023-07-19 12:09:45 +0800597 self.logger.info("Start to generate comparision result between "\
598 "{} and {}."
599 .format(self.old_size_version.revision,
600 self.new_size_version.revision))
601 output_file = os.path.join(
602 self.result_dir,
603 self.gen_file_name(self.old_size_version, self.new_size_version))
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000604
Yanray Wang21127f72023-07-19 12:09:45 +0800605 self.code_size_generator.size_generator_write_comparison(
606 self.old_size_version.revision, self.new_size_version.revision,
607 output_file, self.code_size_common.result_options)
608
Xiaofei Bai2400b502021-10-21 12:22:58 +0000609 return 0
Xiaofei Baibca03e52021-09-09 09:42:37 +0000610
Yanray Wang72b105f2023-05-31 15:20:39 +0800611 def get_comparision_results(self) -> int:
Xiaofei Baibca03e52021-09-09 09:42:37 +0000612 """Compare size of library/*.o between self.old_rev and self.new_rev,
613 and generate the result file."""
Gilles Peskined9071e72022-09-18 21:17:09 +0200614 build_tree.check_repo_path()
Yanray Wang923f9432023-07-17 12:43:00 +0800615 self.gen_code_size_report(self.old_size_version)
616 self.gen_code_size_report(self.new_size_version)
Yanray Wang5e9130a2023-07-17 11:55:54 +0800617 return self.gen_code_size_comparison()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000618
Yanray Wang923f9432023-07-17 12:43:00 +0800619
Xiaofei Bai2400b502021-10-21 12:22:58 +0000620def main():
Yanray Wang502c54f2023-05-31 11:41:36 +0800621 parser = argparse.ArgumentParser(description=(__doc__))
622 group_required = parser.add_argument_group(
623 'required arguments',
624 'required arguments to parse for running ' + os.path.basename(__file__))
625 group_required.add_argument(
626 "-o", "--old-rev", type=str, required=True,
627 help="old revision for comparison.")
628
629 group_optional = parser.add_argument_group(
630 'optional arguments',
631 'optional arguments to parse for running ' + os.path.basename(__file__))
632 group_optional.add_argument(
Xiaofei Baibca03e52021-09-09 09:42:37 +0000633 "-r", "--result-dir", type=str, default="comparison",
634 help="directory where comparison result is stored, \
Yanray Wang502c54f2023-05-31 11:41:36 +0800635 default is comparison")
636 group_optional.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000637 "-n", "--new-rev", type=str, default=None,
638 help="new revision for comparison, default is the current work \
Yanray Wang502c54f2023-05-31 11:41:36 +0800639 directory, including uncommitted changes.")
640 group_optional.add_argument(
Yanray Wang23bd5322023-05-24 11:03:59 +0800641 "-a", "--arch", type=str, default=detect_arch(),
642 choices=list(map(lambda s: s.value, SupportedArch)),
643 help="specify architecture for code size comparison, default is the\
Yanray Wang502c54f2023-05-31 11:41:36 +0800644 host architecture.")
645 group_optional.add_argument(
Yanray Wang6a862582023-05-24 12:24:38 +0800646 "-c", "--config", type=str, default=SupportedConfig.DEFAULT.value,
647 choices=list(map(lambda s: s.value, SupportedConfig)),
648 help="specify configuration type for code size comparison,\
Yanray Wang502c54f2023-05-31 11:41:36 +0800649 default is the current MbedTLS configuration.")
Yanray Wangb664cb72023-07-18 12:28:35 +0800650 group_optional.add_argument(
651 '--markdown', action='store_true', dest='markdown',
652 help="Show comparision of code size in a markdown table\
653 (only show the files that have changed).")
Yanray Wang227576a2023-07-18 14:35:05 +0800654 group_optional.add_argument(
655 '--stdout', action='store_true', dest='stdout',
656 help="Set this option to direct comparison result into sys.stdout.\
657 (Default: file)")
Yanray Wang21127f72023-07-19 12:09:45 +0800658 group_optional.add_argument(
659 '--verbose', action='store_true', dest='verbose',
660 help="Show logs in detail for code size measurement. (Default: False)")
Xiaofei Baibca03e52021-09-09 09:42:37 +0000661 comp_args = parser.parse_args()
662
Yanray Wang21127f72023-07-19 12:09:45 +0800663 logger = logging.getLogger()
664 logging_util.configure_logger(logger)
665 logger.setLevel(logging.DEBUG if comp_args.verbose else logging.INFO)
666
Xiaofei Baibca03e52021-09-09 09:42:37 +0000667 if os.path.isfile(comp_args.result_dir):
Yanray Wang21127f72023-07-19 12:09:45 +0800668 logger.error("{} is not a directory".format(comp_args.result_dir))
Xiaofei Baibca03e52021-09-09 09:42:37 +0000669 parser.exit()
670
Yanray Wange0e27602023-07-14 17:37:45 +0800671 validate_res = CodeSizeCalculator.validate_revision(comp_args.old_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000672 old_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai2400b502021-10-21 12:22:58 +0000673
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000674 if comp_args.new_rev is not None:
Yanray Wange0e27602023-07-14 17:37:45 +0800675 validate_res = CodeSizeCalculator.validate_revision(comp_args.new_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000676 new_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000677 else:
678 new_revision = "current"
Xiaofei Bai2400b502021-10-21 12:22:58 +0000679
Yanray Wang923f9432023-07-17 12:43:00 +0800680 old_size_version = SimpleNamespace(
681 version="old",
682 revision=old_revision,
683 config=comp_args.config,
684 arch=comp_args.arch,
Yanray Wang923f9432023-07-17 12:43:00 +0800685 make_cmd='',
686 )
687 new_size_version = SimpleNamespace(
688 version="new",
689 revision=new_revision,
690 config=comp_args.config,
691 arch=comp_args.arch,
Yanray Wang923f9432023-07-17 12:43:00 +0800692 make_cmd='',
693 )
Yanray Wang802af162023-07-17 14:04:30 +0800694 code_size_common = SimpleNamespace(
Yanray Wang227576a2023-07-18 14:35:05 +0800695 result_options=SimpleNamespace(
696 result_dir=comp_args.result_dir,
697 with_markdown=comp_args.markdown,
698 stdout=comp_args.stdout,
699 ),
Yanray Wang802af162023-07-17 14:04:30 +0800700 host_arch=detect_arch(),
701 measure_cmd='size -t',
702 )
Yanray Wang923f9432023-07-17 12:43:00 +0800703
Yanray Wang21127f72023-07-19 12:09:45 +0800704 logger.info("Measure code size between {}:{}-{} and {}:{}-{} by `{}`."
705 .format(old_size_version.revision, old_size_version.config,
706 old_size_version.arch,
707 new_size_version.revision, old_size_version.config,
708 new_size_version.arch,
709 code_size_common.measure_cmd.strip().split(' ')[0]))
Yanray Wang923f9432023-07-17 12:43:00 +0800710 size_compare = CodeSizeComparison(old_size_version, new_size_version,\
Yanray Wang21127f72023-07-19 12:09:45 +0800711 code_size_common, logger)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000712 return_code = size_compare.get_comparision_results()
713 sys.exit(return_code)
714
Xiaofei Baibca03e52021-09-09 09:42:37 +0000715if __name__ == "__main__":
Xiaofei Bai2400b502021-10-21 12:22:58 +0000716 main()