blob: 0f65b15dbd975789ef63c61e4f3f0e6a28100aaa [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
27import os
Yanray Wang16ebc572023-05-30 18:10:20 +080028import re
Xiaofei Baibca03e52021-09-09 09:42:37 +000029import subprocess
30import sys
Yanray Wang16ebc572023-05-30 18:10:20 +080031import typing
Yanray Wang23bd5322023-05-24 11:03:59 +080032from enum import Enum
Xiaofei Baibca03e52021-09-09 09:42:37 +000033
Yanray Wang16ebc572023-05-30 18:10:20 +080034from mbedtls_dev import typing_util
Gilles Peskined9071e72022-09-18 21:17:09 +020035from mbedtls_dev import build_tree
36
Yanray Wang23bd5322023-05-24 11:03:59 +080037class SupportedArch(Enum):
38 """Supported architecture for code size measurement."""
39 AARCH64 = 'aarch64'
40 AARCH32 = 'aarch32'
Yanray Wangaba71582023-05-29 16:45:56 +080041 ARMV8_M = 'armv8-m'
Yanray Wang23bd5322023-05-24 11:03:59 +080042 X86_64 = 'x86_64'
43 X86 = 'x86'
44
Yanray Wang6a862582023-05-24 12:24:38 +080045CONFIG_TFM_MEDIUM_MBEDCRYPTO_H = "../configs/tfm_mbedcrypto_config_profile_medium.h"
46CONFIG_TFM_MEDIUM_PSA_CRYPTO_H = "../configs/crypto_config_profile_medium.h"
47class SupportedConfig(Enum):
48 """Supported configuration for code size measurement."""
49 DEFAULT = 'default'
50 TFM_MEDIUM = 'tfm-medium'
51
Yanray Wang16ebc572023-05-30 18:10:20 +080052# Static library
53MBEDTLS_STATIC_LIB = {
54 'CRYPTO': 'library/libmbedcrypto.a',
55 'X509': 'library/libmbedx509.a',
56 'TLS': 'library/libmbedtls.a',
57}
58
Yanray Wang23bd5322023-05-24 11:03:59 +080059DETECT_ARCH_CMD = "cc -dM -E - < /dev/null"
60def detect_arch() -> str:
61 """Auto-detect host architecture."""
62 cc_output = subprocess.check_output(DETECT_ARCH_CMD, shell=True).decode()
63 if "__aarch64__" in cc_output:
64 return SupportedArch.AARCH64.value
65 if "__arm__" in cc_output:
66 return SupportedArch.AARCH32.value
67 if "__x86_64__" in cc_output:
68 return SupportedArch.X86_64.value
69 if "__x86__" in cc_output:
70 return SupportedArch.X86.value
71 else:
72 print("Unknown host architecture, cannot auto-detect arch.")
73 sys.exit(1)
Gilles Peskined9071e72022-09-18 21:17:09 +020074
Yanray Wang6a862582023-05-24 12:24:38 +080075class CodeSizeInfo: # pylint: disable=too-few-public-methods
76 """Gather information used to measure code size.
77
78 It collects information about architecture, configuration in order to
79 infer build command for code size measurement.
80 """
81
Yanray Wangc18cd892023-05-31 11:08:04 +080082 SupportedArchConfig = [
83 "-a " + SupportedArch.AARCH64.value + " -c " + SupportedConfig.DEFAULT.value,
84 "-a " + SupportedArch.AARCH32.value + " -c " + SupportedConfig.DEFAULT.value,
85 "-a " + SupportedArch.X86_64.value + " -c " + SupportedConfig.DEFAULT.value,
86 "-a " + SupportedArch.X86.value + " -c " + SupportedConfig.DEFAULT.value,
87 "-a " + SupportedArch.ARMV8_M.value + " -c " + SupportedConfig.TFM_MEDIUM.value,
88 ]
89
Yanray Wang21f17442023-06-01 11:29:06 +080090 def __init__(self, arch: str, config: str, sys_arch: str) -> None:
Yanray Wang6a862582023-05-24 12:24:38 +080091 """
92 arch: architecture to measure code size on.
93 config: configuration type to measure code size with.
94 make_command: command to build library (Inferred from arch and config).
95 """
96 self.arch = arch
97 self.config = config
Yanray Wang21f17442023-06-01 11:29:06 +080098 self.sys_arch = sys_arch
Yanray Wang6a862582023-05-24 12:24:38 +080099 self.make_command = self.set_make_command()
100
101 def set_make_command(self) -> str:
102 """Infer build command based on architecture and configuration."""
103
Yanray Wang21f17442023-06-01 11:29:06 +0800104 if self.config == SupportedConfig.DEFAULT.value and \
105 self.arch == self.sys_arch:
Yanray Wang6a862582023-05-24 12:24:38 +0800106 return 'make -j lib CFLAGS=\'-Os \' '
Yanray Wangaba71582023-05-29 16:45:56 +0800107 elif self.arch == SupportedArch.ARMV8_M.value and \
Yanray Wang6a862582023-05-24 12:24:38 +0800108 self.config == SupportedConfig.TFM_MEDIUM.value:
109 return \
Yanray Wang60430bd2023-05-29 14:48:18 +0800110 'make -j lib CC=armclang \
Yanray Wang6a862582023-05-24 12:24:38 +0800111 CFLAGS=\'--target=arm-arm-none-eabi -mcpu=cortex-m33 -Os \
112 -DMBEDTLS_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_MBEDCRYPTO_H + '\\\" \
113 -DMBEDTLS_PSA_CRYPTO_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_PSA_CRYPTO_H + '\\\" \''
114 else:
Yanray Wang21f17442023-06-01 11:29:06 +0800115 print("Unsupported combination of architecture: {} and configuration: {}"
Yanray Wang6a862582023-05-24 12:24:38 +0800116 .format(self.arch, self.config))
Yanray Wangc18cd892023-05-31 11:08:04 +0800117 print("\nPlease use supported combination of architecture and configuration:")
118 for comb in CodeSizeInfo.SupportedArchConfig:
119 print(comb)
Yanray Wang21f17442023-06-01 11:29:06 +0800120 print("\nFor your system, please use:")
121 for comb in CodeSizeInfo.SupportedArchConfig:
122 if "default" in comb and self.sys_arch not in comb:
123 continue
124 print(comb)
Yanray Wang6a862582023-05-24 12:24:38 +0800125 sys.exit(1)
126
Yanray Wang16ebc572023-05-30 18:10:20 +0800127class SizeEntry: # pylint: disable=too-few-public-methods
128 """Data Structure to only store information of code size."""
129 def __init__(self, text, data, bss, dec):
130 self.text = text
131 self.data = data
132 self.bss = bss
133 self.total = dec # total <=> dec
Yanray Wang6a862582023-05-24 12:24:38 +0800134
Yanray Wang16ebc572023-05-30 18:10:20 +0800135class CodeSizeBase:
136 """Code Size Base Class for size record saving and writing."""
137
138 def __init__(self) -> None:
139 """ Variable code_size is used to store size info for any revisions.
140 code_size: (data format)
141 {revision: {module: {file_name: SizeEntry,
142 etc ...
143 },
144 etc ...
145 },
146 etc ...
147 }
148 """
149 self.code_size = {} #type: typing.Dict[str, typing.Dict]
150
151 def set_size_record(self, revision: str, mod: str, size_text: str) -> None:
152 """Store size information for target revision and high-level module.
153
154 size_text Format: text data bss dec hex filename
155 """
156 size_record = {}
157 for line in size_text.splitlines()[1:]:
158 data = line.split()
159 size_record[data[5]] = SizeEntry(data[0], data[1], data[2], data[3])
160 if revision in self.code_size:
161 self.code_size[revision].update({mod: size_record})
162 else:
163 self.code_size[revision] = {mod: size_record}
164
165 def read_size_record(self, revision: str, fname: str) -> None:
166 """Read size information from csv file and write it into code_size.
167
168 fname Format: filename text data bss dec
169 """
170 mod = ""
171 size_record = {}
172 with open(fname, 'r') as csv_file:
173 for line in csv_file:
174 data = line.strip().split()
175 # check if we find the beginning of a module
176 if data and data[0] in MBEDTLS_STATIC_LIB:
177 mod = data[0]
178 continue
179
180 if mod:
181 size_record[data[0]] = \
182 SizeEntry(data[1], data[2], data[3], data[4])
183
184 # check if we hit record for the end of a module
185 m = re.match(r'.?TOTALS', line)
186 if m:
187 if revision in self.code_size:
188 self.code_size[revision].update({mod: size_record})
189 else:
190 self.code_size[revision] = {mod: size_record}
191 mod = ""
192 size_record = {}
193
194 def _size_reader_helper(
195 self,
196 revision: str,
197 output: typing_util.Writable
198 ) -> typing.Iterator[tuple]:
199 """A helper function to peel code_size based on revision."""
200 for mod, file_size in self.code_size[revision].items():
201 output.write("\n" + mod + "\n")
202 for fname, size_entry in file_size.items():
203 yield mod, fname, size_entry
204
205 def write_size_record(
206 self,
207 revision: str,
208 output: typing_util.Writable
209 ) -> None:
210 """Write size information to a file.
211
212 Writing Format: file_name text data bss total(dec)
213 """
214 output.write("{:<30} {:>7} {:>7} {:>7} {:>7}\n"
215 .format("filename", "text", "data", "bss", "total"))
216 for _, fname, size_entry in self._size_reader_helper(revision, output):
217 output.write("{:<30} {:>7} {:>7} {:>7} {:>7}\n"
218 .format(fname, size_entry.text, size_entry.data,\
219 size_entry.bss, size_entry.total))
220
221 def write_comparison(
222 self,
223 old_rev: str,
224 new_rev: str,
225 output: typing_util.Writable
226 ) -> None:
227 """Write comparison result into a file.
228
229 Writing Format: file_name current(total) old(total) change(Byte) change_pct(%)
230 """
231 output.write("{:<30} {:>7} {:>7} {:>7} {:>7}\n"
232 .format("filename", "current", "old", "change", "change%"))
233 for mod, fname, size_entry in self._size_reader_helper(new_rev, output):
234 new_size = int(size_entry.total)
235 # check if we have the file in old revision
236 if fname in self.code_size[old_rev][mod]:
237 old_size = int(self.code_size[old_rev][mod][fname].total)
238 change = new_size - old_size
239 if old_size != 0:
240 change_pct = change / old_size
241 else:
242 change_pct = 0
243 output.write("{:<30} {:>7} {:>7} {:>7} {:>7.2%}\n"
244 .format(fname, new_size, old_size, change, change_pct))
245 else:
246 output.write("{} {}\n".format(fname, new_size))
247
248
249class CodeSizeComparison(CodeSizeBase):
Xiaofei Bai2400b502021-10-21 12:22:58 +0000250 """Compare code size between two Git revisions."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000251
Yanray Wang6a862582023-05-24 12:24:38 +0800252 def __init__(self, old_revision, new_revision, result_dir, code_size_info):
Xiaofei Baibca03e52021-09-09 09:42:37 +0000253 """
Yanray Wang6a862582023-05-24 12:24:38 +0800254 old_revision: revision to compare against.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000255 new_revision:
Yanray Wang6a862582023-05-24 12:24:38 +0800256 result_dir: directory for comparison result.
257 code_size_info: an object containing information to build library.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000258 """
Yanray Wang8804db92023-05-30 18:18:18 +0800259 super().__init__()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000260 self.repo_path = "."
261 self.result_dir = os.path.abspath(result_dir)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000262 os.makedirs(self.result_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000263
264 self.csv_dir = os.path.abspath("code_size_records/")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000265 os.makedirs(self.csv_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000266
267 self.old_rev = old_revision
268 self.new_rev = new_revision
269 self.git_command = "git"
Yanray Wang6a862582023-05-24 12:24:38 +0800270 self.make_command = code_size_info.make_command
Yanray Wang369cd962023-05-24 17:13:29 +0800271 self.fname_suffix = "-" + code_size_info.arch + "-" +\
272 code_size_info.config
Xiaofei Baibca03e52021-09-09 09:42:37 +0000273
274 @staticmethod
Xiaofei Bai2400b502021-10-21 12:22:58 +0000275 def validate_revision(revision):
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000276 result = subprocess.check_output(["git", "rev-parse", "--verify",
277 revision + "^{commit}"], shell=False)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000278 return result
Xiaofei Bai2400b502021-10-21 12:22:58 +0000279
Xiaofei Baibca03e52021-09-09 09:42:37 +0000280 def _create_git_worktree(self, revision):
281 """Make a separate worktree for revision.
282 Do not modify the current worktree."""
283
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000284 if revision == "current":
Xiaofei Baibca03e52021-09-09 09:42:37 +0000285 print("Using current work directory.")
286 git_worktree_path = self.repo_path
287 else:
288 print("Creating git worktree for", revision)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000289 git_worktree_path = os.path.join(self.repo_path, "temp-" + revision)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000290 subprocess.check_output(
291 [self.git_command, "worktree", "add", "--detach",
292 git_worktree_path, revision], cwd=self.repo_path,
293 stderr=subprocess.STDOUT
294 )
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100295
Xiaofei Baibca03e52021-09-09 09:42:37 +0000296 return git_worktree_path
297
298 def _build_libraries(self, git_worktree_path):
299 """Build libraries in the specified worktree."""
300
301 my_environment = os.environ.copy()
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100302 try:
303 subprocess.check_output(
304 self.make_command, env=my_environment, shell=True,
305 cwd=git_worktree_path, stderr=subprocess.STDOUT,
306 )
307 except subprocess.CalledProcessError as e:
308 self._handle_called_process_error(e, git_worktree_path)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000309
310 def _gen_code_size_csv(self, revision, git_worktree_path):
311 """Generate code size csv file."""
312
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000313 if revision == "current":
314 print("Measuring code size in current work directory.")
315 else:
316 print("Measuring code size for", revision)
Yanray Wang8804db92023-05-30 18:18:18 +0800317
318 for mod, st_lib in MBEDTLS_STATIC_LIB.items():
319 try:
320 result = subprocess.check_output(
321 ["size", st_lib, "-t"], cwd=git_worktree_path
322 )
323 except subprocess.CalledProcessError as e:
324 self._handle_called_process_error(e, git_worktree_path)
325 size_text = result.decode("utf-8")
326
327 self.set_size_record(revision, mod, size_text)
328
329 print("Generating code size csv for", revision)
330 csv_file = open(os.path.join(self.csv_dir, revision +
331 self.fname_suffix + ".csv"), "w")
332 self.write_size_record(revision, csv_file)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000333
334 def _remove_worktree(self, git_worktree_path):
335 """Remove temporary worktree."""
336 if git_worktree_path != self.repo_path:
337 print("Removing temporary worktree", git_worktree_path)
338 subprocess.check_output(
339 [self.git_command, "worktree", "remove", "--force",
340 git_worktree_path], cwd=self.repo_path,
341 stderr=subprocess.STDOUT
342 )
343
344 def _get_code_size_for_rev(self, revision):
345 """Generate code size csv file for the specified git revision."""
346
347 # Check if the corresponding record exists
Yanray Wang369cd962023-05-24 17:13:29 +0800348 csv_fname = revision + self.fname_suffix + ".csv"
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000349 if (revision != "current") and \
Xiaofei Baibca03e52021-09-09 09:42:37 +0000350 os.path.exists(os.path.join(self.csv_dir, csv_fname)):
351 print("Code size csv file for", revision, "already exists.")
Yanray Wang8804db92023-05-30 18:18:18 +0800352 self.read_size_record(revision, os.path.join(self.csv_dir, csv_fname))
Xiaofei Baibca03e52021-09-09 09:42:37 +0000353 else:
354 git_worktree_path = self._create_git_worktree(revision)
355 self._build_libraries(git_worktree_path)
356 self._gen_code_size_csv(revision, git_worktree_path)
357 self._remove_worktree(git_worktree_path)
358
Yanray Wang8804db92023-05-30 18:18:18 +0800359 def _gen_code_size_comparison(self):
Xiaofei Baibca03e52021-09-09 09:42:37 +0000360 """Generate results of the size changes between two revisions,
361 old and new. Measured code size results of these two revisions
Xiaofei Bai2400b502021-10-21 12:22:58 +0000362 must be available."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000363
Yanray Wang369cd962023-05-24 17:13:29 +0800364 res_file = open(os.path.join(self.result_dir, "compare-" +
365 self.old_rev + "-" + self.new_rev +
366 self.fname_suffix +
367 ".csv"), "w")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000368
Shaun Case8b0ecbc2021-12-20 21:14:10 -0800369 print("Generating comparison results.")
Yanray Wang8804db92023-05-30 18:18:18 +0800370 self.write_comparison(self.old_rev, self.new_rev, res_file)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000371
Xiaofei Bai2400b502021-10-21 12:22:58 +0000372 return 0
Xiaofei Baibca03e52021-09-09 09:42:37 +0000373
374 def get_comparision_results(self):
375 """Compare size of library/*.o between self.old_rev and self.new_rev,
376 and generate the result file."""
Gilles Peskined9071e72022-09-18 21:17:09 +0200377 build_tree.check_repo_path()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000378 self._get_code_size_for_rev(self.old_rev)
379 self._get_code_size_for_rev(self.new_rev)
Yanray Wang8804db92023-05-30 18:18:18 +0800380 return self._gen_code_size_comparison()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000381
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100382 def _handle_called_process_error(self, e: subprocess.CalledProcessError,
383 git_worktree_path):
384 """Handle a CalledProcessError and quit the program gracefully.
385 Remove any extra worktrees so that the script may be called again."""
386
387 # Tell the user what went wrong
388 print("The following command: {} failed and exited with code {}"
389 .format(e.cmd, e.returncode))
390 print("Process output:\n {}".format(str(e.output, "utf-8")))
391
392 # Quit gracefully by removing the existing worktree
393 self._remove_worktree(git_worktree_path)
394 sys.exit(-1)
395
Xiaofei Bai2400b502021-10-21 12:22:58 +0000396def main():
Yanray Wang502c54f2023-05-31 11:41:36 +0800397 parser = argparse.ArgumentParser(description=(__doc__))
398 group_required = parser.add_argument_group(
399 'required arguments',
400 'required arguments to parse for running ' + os.path.basename(__file__))
401 group_required.add_argument(
402 "-o", "--old-rev", type=str, required=True,
403 help="old revision for comparison.")
404
405 group_optional = parser.add_argument_group(
406 'optional arguments',
407 'optional arguments to parse for running ' + os.path.basename(__file__))
408 group_optional.add_argument(
Xiaofei Baibca03e52021-09-09 09:42:37 +0000409 "-r", "--result-dir", type=str, default="comparison",
410 help="directory where comparison result is stored, \
Yanray Wang502c54f2023-05-31 11:41:36 +0800411 default is comparison")
412 group_optional.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000413 "-n", "--new-rev", type=str, default=None,
414 help="new revision for comparison, default is the current work \
Yanray Wang502c54f2023-05-31 11:41:36 +0800415 directory, including uncommitted changes.")
416 group_optional.add_argument(
Yanray Wang23bd5322023-05-24 11:03:59 +0800417 "-a", "--arch", type=str, default=detect_arch(),
418 choices=list(map(lambda s: s.value, SupportedArch)),
419 help="specify architecture for code size comparison, default is the\
Yanray Wang502c54f2023-05-31 11:41:36 +0800420 host architecture.")
421 group_optional.add_argument(
Yanray Wang6a862582023-05-24 12:24:38 +0800422 "-c", "--config", type=str, default=SupportedConfig.DEFAULT.value,
423 choices=list(map(lambda s: s.value, SupportedConfig)),
424 help="specify configuration type for code size comparison,\
Yanray Wang502c54f2023-05-31 11:41:36 +0800425 default is the current MbedTLS configuration.")
Xiaofei Baibca03e52021-09-09 09:42:37 +0000426 comp_args = parser.parse_args()
427
428 if os.path.isfile(comp_args.result_dir):
429 print("Error: {} is not a directory".format(comp_args.result_dir))
430 parser.exit()
431
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000432 validate_res = CodeSizeComparison.validate_revision(comp_args.old_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000433 old_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai2400b502021-10-21 12:22:58 +0000434
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000435 if comp_args.new_rev is not None:
436 validate_res = CodeSizeComparison.validate_revision(comp_args.new_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000437 new_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000438 else:
439 new_revision = "current"
Xiaofei Bai2400b502021-10-21 12:22:58 +0000440
Yanray Wang21f17442023-06-01 11:29:06 +0800441 code_size_info = CodeSizeInfo(comp_args.arch, comp_args.config,
442 detect_arch())
Yanray Wangaba71582023-05-29 16:45:56 +0800443 print("Measure code size for architecture: {}, configuration: {}"
444 .format(code_size_info.arch, code_size_info.config))
Xiaofei Baibca03e52021-09-09 09:42:37 +0000445 result_dir = comp_args.result_dir
Yanray Wang6a862582023-05-24 12:24:38 +0800446 size_compare = CodeSizeComparison(old_revision, new_revision, result_dir,
447 code_size_info)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000448 return_code = size_compare.get_comparision_results()
449 sys.exit(return_code)
450
451
452if __name__ == "__main__":
Xiaofei Bai2400b502021-10-21 12:22:58 +0000453 main()