blob: f9e672dd08969b3471cc3ea23df0b2d483fc225d [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
28import subprocess
29import sys
Yanray Wang23bd5322023-05-24 11:03:59 +080030from enum import Enum
Xiaofei Baibca03e52021-09-09 09:42:37 +000031
Gilles Peskined9071e72022-09-18 21:17:09 +020032from mbedtls_dev import build_tree
33
Yanray Wang23bd5322023-05-24 11:03:59 +080034class SupportedArch(Enum):
35 """Supported architecture for code size measurement."""
36 AARCH64 = 'aarch64'
37 AARCH32 = 'aarch32'
Yanray Wangaba71582023-05-29 16:45:56 +080038 ARMV8_M = 'armv8-m'
Yanray Wang23bd5322023-05-24 11:03:59 +080039 X86_64 = 'x86_64'
40 X86 = 'x86'
41
Yanray Wang6a862582023-05-24 12:24:38 +080042CONFIG_TFM_MEDIUM_MBEDCRYPTO_H = "../configs/tfm_mbedcrypto_config_profile_medium.h"
43CONFIG_TFM_MEDIUM_PSA_CRYPTO_H = "../configs/crypto_config_profile_medium.h"
44class SupportedConfig(Enum):
45 """Supported configuration for code size measurement."""
46 DEFAULT = 'default'
47 TFM_MEDIUM = 'tfm-medium'
48
Yanray Wang23bd5322023-05-24 11:03:59 +080049DETECT_ARCH_CMD = "cc -dM -E - < /dev/null"
50def detect_arch() -> str:
51 """Auto-detect host architecture."""
52 cc_output = subprocess.check_output(DETECT_ARCH_CMD, shell=True).decode()
53 if "__aarch64__" in cc_output:
54 return SupportedArch.AARCH64.value
55 if "__arm__" in cc_output:
56 return SupportedArch.AARCH32.value
57 if "__x86_64__" in cc_output:
58 return SupportedArch.X86_64.value
59 if "__x86__" in cc_output:
60 return SupportedArch.X86.value
61 else:
62 print("Unknown host architecture, cannot auto-detect arch.")
63 sys.exit(1)
Gilles Peskined9071e72022-09-18 21:17:09 +020064
Yanray Wang6a862582023-05-24 12:24:38 +080065class CodeSizeInfo: # pylint: disable=too-few-public-methods
66 """Gather information used to measure code size.
67
68 It collects information about architecture, configuration in order to
69 infer build command for code size measurement.
70 """
71
Yanray Wangc18cd892023-05-31 11:08:04 +080072 SupportedArchConfig = [
73 "-a " + SupportedArch.AARCH64.value + " -c " + SupportedConfig.DEFAULT.value,
74 "-a " + SupportedArch.AARCH32.value + " -c " + SupportedConfig.DEFAULT.value,
75 "-a " + SupportedArch.X86_64.value + " -c " + SupportedConfig.DEFAULT.value,
76 "-a " + SupportedArch.X86.value + " -c " + SupportedConfig.DEFAULT.value,
77 "-a " + SupportedArch.ARMV8_M.value + " -c " + SupportedConfig.TFM_MEDIUM.value,
78 ]
79
Yanray Wang6a862582023-05-24 12:24:38 +080080 def __init__(self, arch: str, config: str) -> None:
81 """
82 arch: architecture to measure code size on.
83 config: configuration type to measure code size with.
84 make_command: command to build library (Inferred from arch and config).
85 """
86 self.arch = arch
87 self.config = config
88 self.make_command = self.set_make_command()
89
90 def set_make_command(self) -> str:
91 """Infer build command based on architecture and configuration."""
92
93 if self.config == SupportedConfig.DEFAULT.value:
94 return 'make -j lib CFLAGS=\'-Os \' '
Yanray Wangaba71582023-05-29 16:45:56 +080095 elif self.arch == SupportedArch.ARMV8_M.value and \
Yanray Wang6a862582023-05-24 12:24:38 +080096 self.config == SupportedConfig.TFM_MEDIUM.value:
97 return \
Yanray Wang60430bd2023-05-29 14:48:18 +080098 'make -j lib CC=armclang \
Yanray Wang6a862582023-05-24 12:24:38 +080099 CFLAGS=\'--target=arm-arm-none-eabi -mcpu=cortex-m33 -Os \
100 -DMBEDTLS_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_MBEDCRYPTO_H + '\\\" \
101 -DMBEDTLS_PSA_CRYPTO_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_PSA_CRYPTO_H + '\\\" \''
102 else:
103 print("Unsupported architecture: {} and configurations: {}"
104 .format(self.arch, self.config))
Yanray Wangc18cd892023-05-31 11:08:04 +0800105 print("\nPlease use supported combination of architecture and configuration:")
106 for comb in CodeSizeInfo.SupportedArchConfig:
107 print(comb)
Yanray Wang6a862582023-05-24 12:24:38 +0800108 sys.exit(1)
109
110
Xiaofei Baibca03e52021-09-09 09:42:37 +0000111class CodeSizeComparison:
Xiaofei Bai2400b502021-10-21 12:22:58 +0000112 """Compare code size between two Git revisions."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000113
Yanray Wang6a862582023-05-24 12:24:38 +0800114 def __init__(self, old_revision, new_revision, result_dir, code_size_info):
Xiaofei Baibca03e52021-09-09 09:42:37 +0000115 """
Yanray Wang6a862582023-05-24 12:24:38 +0800116 old_revision: revision to compare against.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000117 new_revision:
Yanray Wang6a862582023-05-24 12:24:38 +0800118 result_dir: directory for comparison result.
119 code_size_info: an object containing information to build library.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000120 """
121 self.repo_path = "."
122 self.result_dir = os.path.abspath(result_dir)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000123 os.makedirs(self.result_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000124
125 self.csv_dir = os.path.abspath("code_size_records/")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000126 os.makedirs(self.csv_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000127
128 self.old_rev = old_revision
129 self.new_rev = new_revision
130 self.git_command = "git"
Yanray Wang6a862582023-05-24 12:24:38 +0800131 self.make_command = code_size_info.make_command
Yanray Wang369cd962023-05-24 17:13:29 +0800132 self.fname_suffix = "-" + code_size_info.arch + "-" +\
133 code_size_info.config
Xiaofei Baibca03e52021-09-09 09:42:37 +0000134
135 @staticmethod
Xiaofei Bai2400b502021-10-21 12:22:58 +0000136 def validate_revision(revision):
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000137 result = subprocess.check_output(["git", "rev-parse", "--verify",
138 revision + "^{commit}"], shell=False)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000139 return result
Xiaofei Bai2400b502021-10-21 12:22:58 +0000140
Xiaofei Baibca03e52021-09-09 09:42:37 +0000141 def _create_git_worktree(self, revision):
142 """Make a separate worktree for revision.
143 Do not modify the current worktree."""
144
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000145 if revision == "current":
Xiaofei Baibca03e52021-09-09 09:42:37 +0000146 print("Using current work directory.")
147 git_worktree_path = self.repo_path
148 else:
149 print("Creating git worktree for", revision)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000150 git_worktree_path = os.path.join(self.repo_path, "temp-" + revision)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000151 subprocess.check_output(
152 [self.git_command, "worktree", "add", "--detach",
153 git_worktree_path, revision], cwd=self.repo_path,
154 stderr=subprocess.STDOUT
155 )
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100156
Xiaofei Baibca03e52021-09-09 09:42:37 +0000157 return git_worktree_path
158
159 def _build_libraries(self, git_worktree_path):
160 """Build libraries in the specified worktree."""
161
162 my_environment = os.environ.copy()
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100163 try:
164 subprocess.check_output(
165 self.make_command, env=my_environment, shell=True,
166 cwd=git_worktree_path, stderr=subprocess.STDOUT,
167 )
168 except subprocess.CalledProcessError as e:
169 self._handle_called_process_error(e, git_worktree_path)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000170
171 def _gen_code_size_csv(self, revision, git_worktree_path):
172 """Generate code size csv file."""
173
Yanray Wang369cd962023-05-24 17:13:29 +0800174 csv_fname = revision + self.fname_suffix + ".csv"
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000175 if revision == "current":
176 print("Measuring code size in current work directory.")
177 else:
178 print("Measuring code size for", revision)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000179 result = subprocess.check_output(
180 ["size library/*.o"], cwd=git_worktree_path, shell=True
181 )
182 size_text = result.decode()
183 csv_file = open(os.path.join(self.csv_dir, csv_fname), "w")
184 for line in size_text.splitlines()[1:]:
185 data = line.split()
186 csv_file.write("{}, {}\n".format(data[5], data[3]))
187
188 def _remove_worktree(self, git_worktree_path):
189 """Remove temporary worktree."""
190 if git_worktree_path != self.repo_path:
191 print("Removing temporary worktree", git_worktree_path)
192 subprocess.check_output(
193 [self.git_command, "worktree", "remove", "--force",
194 git_worktree_path], cwd=self.repo_path,
195 stderr=subprocess.STDOUT
196 )
197
198 def _get_code_size_for_rev(self, revision):
199 """Generate code size csv file for the specified git revision."""
200
201 # Check if the corresponding record exists
Yanray Wang369cd962023-05-24 17:13:29 +0800202 csv_fname = revision + self.fname_suffix + ".csv"
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000203 if (revision != "current") and \
Xiaofei Baibca03e52021-09-09 09:42:37 +0000204 os.path.exists(os.path.join(self.csv_dir, csv_fname)):
205 print("Code size csv file for", revision, "already exists.")
206 else:
207 git_worktree_path = self._create_git_worktree(revision)
208 self._build_libraries(git_worktree_path)
209 self._gen_code_size_csv(revision, git_worktree_path)
210 self._remove_worktree(git_worktree_path)
211
212 def compare_code_size(self):
213 """Generate results of the size changes between two revisions,
214 old and new. Measured code size results of these two revisions
Xiaofei Bai2400b502021-10-21 12:22:58 +0000215 must be available."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000216
Yanray Wang369cd962023-05-24 17:13:29 +0800217 old_file = open(os.path.join(self.csv_dir, self.old_rev +
218 self.fname_suffix + ".csv"), "r")
219 new_file = open(os.path.join(self.csv_dir, self.new_rev +
220 self.fname_suffix + ".csv"), "r")
221 res_file = open(os.path.join(self.result_dir, "compare-" +
222 self.old_rev + "-" + self.new_rev +
223 self.fname_suffix +
224 ".csv"), "w")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000225
Xiaofei Baibca03e52021-09-09 09:42:37 +0000226 res_file.write("file_name, this_size, old_size, change, change %\n")
Shaun Case8b0ecbc2021-12-20 21:14:10 -0800227 print("Generating comparison results.")
Xiaofei Baibca03e52021-09-09 09:42:37 +0000228
229 old_ds = {}
Yanray Wanga3841ab2023-05-24 18:33:08 +0800230 for line in old_file.readlines():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000231 cols = line.split(", ")
232 fname = cols[0]
233 size = int(cols[1])
234 if size != 0:
235 old_ds[fname] = size
236
237 new_ds = {}
Yanray Wanga3841ab2023-05-24 18:33:08 +0800238 for line in new_file.readlines():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000239 cols = line.split(", ")
240 fname = cols[0]
241 size = int(cols[1])
242 new_ds[fname] = size
243
244 for fname in new_ds:
245 this_size = new_ds[fname]
246 if fname in old_ds:
247 old_size = old_ds[fname]
248 change = this_size - old_size
249 change_pct = change / old_size
250 res_file.write("{}, {}, {}, {}, {:.2%}\n".format(fname, \
251 this_size, old_size, change, float(change_pct)))
252 else:
253 res_file.write("{}, {}\n".format(fname, this_size))
Xiaofei Bai2400b502021-10-21 12:22:58 +0000254 return 0
Xiaofei Baibca03e52021-09-09 09:42:37 +0000255
256 def get_comparision_results(self):
257 """Compare size of library/*.o between self.old_rev and self.new_rev,
258 and generate the result file."""
Gilles Peskined9071e72022-09-18 21:17:09 +0200259 build_tree.check_repo_path()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000260 self._get_code_size_for_rev(self.old_rev)
261 self._get_code_size_for_rev(self.new_rev)
262 return self.compare_code_size()
263
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100264 def _handle_called_process_error(self, e: subprocess.CalledProcessError,
265 git_worktree_path):
266 """Handle a CalledProcessError and quit the program gracefully.
267 Remove any extra worktrees so that the script may be called again."""
268
269 # Tell the user what went wrong
270 print("The following command: {} failed and exited with code {}"
271 .format(e.cmd, e.returncode))
272 print("Process output:\n {}".format(str(e.output, "utf-8")))
273
274 # Quit gracefully by removing the existing worktree
275 self._remove_worktree(git_worktree_path)
276 sys.exit(-1)
277
Xiaofei Bai2400b502021-10-21 12:22:58 +0000278def main():
Yanray Wang502c54f2023-05-31 11:41:36 +0800279 parser = argparse.ArgumentParser(description=(__doc__))
280 group_required = parser.add_argument_group(
281 'required arguments',
282 'required arguments to parse for running ' + os.path.basename(__file__))
283 group_required.add_argument(
284 "-o", "--old-rev", type=str, required=True,
285 help="old revision for comparison.")
286
287 group_optional = parser.add_argument_group(
288 'optional arguments',
289 'optional arguments to parse for running ' + os.path.basename(__file__))
290 group_optional.add_argument(
Xiaofei Baibca03e52021-09-09 09:42:37 +0000291 "-r", "--result-dir", type=str, default="comparison",
292 help="directory where comparison result is stored, \
Yanray Wang502c54f2023-05-31 11:41:36 +0800293 default is comparison")
294 group_optional.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000295 "-n", "--new-rev", type=str, default=None,
296 help="new revision for comparison, default is the current work \
Yanray Wang502c54f2023-05-31 11:41:36 +0800297 directory, including uncommitted changes.")
298 group_optional.add_argument(
Yanray Wang23bd5322023-05-24 11:03:59 +0800299 "-a", "--arch", type=str, default=detect_arch(),
300 choices=list(map(lambda s: s.value, SupportedArch)),
301 help="specify architecture for code size comparison, default is the\
Yanray Wang502c54f2023-05-31 11:41:36 +0800302 host architecture.")
303 group_optional.add_argument(
Yanray Wang6a862582023-05-24 12:24:38 +0800304 "-c", "--config", type=str, default=SupportedConfig.DEFAULT.value,
305 choices=list(map(lambda s: s.value, SupportedConfig)),
306 help="specify configuration type for code size comparison,\
Yanray Wang502c54f2023-05-31 11:41:36 +0800307 default is the current MbedTLS configuration.")
Xiaofei Baibca03e52021-09-09 09:42:37 +0000308 comp_args = parser.parse_args()
309
310 if os.path.isfile(comp_args.result_dir):
311 print("Error: {} is not a directory".format(comp_args.result_dir))
312 parser.exit()
313
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000314 validate_res = CodeSizeComparison.validate_revision(comp_args.old_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000315 old_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai2400b502021-10-21 12:22:58 +0000316
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000317 if comp_args.new_rev is not None:
318 validate_res = CodeSizeComparison.validate_revision(comp_args.new_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000319 new_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000320 else:
321 new_revision = "current"
Xiaofei Bai2400b502021-10-21 12:22:58 +0000322
Yanray Wang6a862582023-05-24 12:24:38 +0800323 code_size_info = CodeSizeInfo(comp_args.arch, comp_args.config)
Yanray Wangaba71582023-05-29 16:45:56 +0800324 print("Measure code size for architecture: {}, configuration: {}"
325 .format(code_size_info.arch, code_size_info.config))
Xiaofei Baibca03e52021-09-09 09:42:37 +0000326 result_dir = comp_args.result_dir
Yanray Wang6a862582023-05-24 12:24:38 +0800327 size_compare = CodeSizeComparison(old_revision, new_revision, result_dir,
328 code_size_info)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000329 return_code = size_compare.get_comparision_results()
330 sys.exit(return_code)
331
332
333if __name__ == "__main__":
Xiaofei Bai2400b502021-10-21 12:22:58 +0000334 main()