blob: de5249a5e77ae2c324f14be48d80214791113cbd [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 Wang21f17442023-06-01 11:29:06 +080080 def __init__(self, arch: str, config: str, sys_arch: str) -> None:
Yanray Wang6a862582023-05-24 12:24:38 +080081 """
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
Yanray Wang21f17442023-06-01 11:29:06 +080088 self.sys_arch = sys_arch
Yanray Wang6a862582023-05-24 12:24:38 +080089 self.make_command = self.set_make_command()
90
91 def set_make_command(self) -> str:
92 """Infer build command based on architecture and configuration."""
93
Yanray Wang21f17442023-06-01 11:29:06 +080094 if self.config == SupportedConfig.DEFAULT.value and \
95 self.arch == self.sys_arch:
Yanray Wang6a862582023-05-24 12:24:38 +080096 return 'make -j lib CFLAGS=\'-Os \' '
Yanray Wangaba71582023-05-29 16:45:56 +080097 elif self.arch == SupportedArch.ARMV8_M.value and \
Yanray Wang6a862582023-05-24 12:24:38 +080098 self.config == SupportedConfig.TFM_MEDIUM.value:
99 return \
Yanray Wang60430bd2023-05-29 14:48:18 +0800100 'make -j lib CC=armclang \
Yanray Wang6a862582023-05-24 12:24:38 +0800101 CFLAGS=\'--target=arm-arm-none-eabi -mcpu=cortex-m33 -Os \
102 -DMBEDTLS_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_MBEDCRYPTO_H + '\\\" \
103 -DMBEDTLS_PSA_CRYPTO_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_PSA_CRYPTO_H + '\\\" \''
104 else:
Yanray Wang21f17442023-06-01 11:29:06 +0800105 print("Unsupported combination of architecture: {} and configuration: {}"
Yanray Wang6a862582023-05-24 12:24:38 +0800106 .format(self.arch, self.config))
Yanray Wangc18cd892023-05-31 11:08:04 +0800107 print("\nPlease use supported combination of architecture and configuration:")
108 for comb in CodeSizeInfo.SupportedArchConfig:
109 print(comb)
Yanray Wang21f17442023-06-01 11:29:06 +0800110 print("\nFor your system, please use:")
111 for comb in CodeSizeInfo.SupportedArchConfig:
112 if "default" in comb and self.sys_arch not in comb:
113 continue
114 print(comb)
Yanray Wang6a862582023-05-24 12:24:38 +0800115 sys.exit(1)
116
117
Xiaofei Baibca03e52021-09-09 09:42:37 +0000118class CodeSizeComparison:
Xiaofei Bai2400b502021-10-21 12:22:58 +0000119 """Compare code size between two Git revisions."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000120
Yanray Wang6a862582023-05-24 12:24:38 +0800121 def __init__(self, old_revision, new_revision, result_dir, code_size_info):
Xiaofei Baibca03e52021-09-09 09:42:37 +0000122 """
Yanray Wang6a862582023-05-24 12:24:38 +0800123 old_revision: revision to compare against.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000124 new_revision:
Yanray Wang6a862582023-05-24 12:24:38 +0800125 result_dir: directory for comparison result.
126 code_size_info: an object containing information to build library.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000127 """
128 self.repo_path = "."
129 self.result_dir = os.path.abspath(result_dir)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000130 os.makedirs(self.result_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000131
132 self.csv_dir = os.path.abspath("code_size_records/")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000133 os.makedirs(self.csv_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000134
135 self.old_rev = old_revision
136 self.new_rev = new_revision
137 self.git_command = "git"
Yanray Wang6a862582023-05-24 12:24:38 +0800138 self.make_command = code_size_info.make_command
Yanray Wang369cd962023-05-24 17:13:29 +0800139 self.fname_suffix = "-" + code_size_info.arch + "-" +\
140 code_size_info.config
Xiaofei Baibca03e52021-09-09 09:42:37 +0000141
142 @staticmethod
Xiaofei Bai2400b502021-10-21 12:22:58 +0000143 def validate_revision(revision):
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000144 result = subprocess.check_output(["git", "rev-parse", "--verify",
145 revision + "^{commit}"], shell=False)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000146 return result
Xiaofei Bai2400b502021-10-21 12:22:58 +0000147
Xiaofei Baibca03e52021-09-09 09:42:37 +0000148 def _create_git_worktree(self, revision):
149 """Make a separate worktree for revision.
150 Do not modify the current worktree."""
151
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000152 if revision == "current":
Xiaofei Baibca03e52021-09-09 09:42:37 +0000153 print("Using current work directory.")
154 git_worktree_path = self.repo_path
155 else:
156 print("Creating git worktree for", revision)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000157 git_worktree_path = os.path.join(self.repo_path, "temp-" + revision)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000158 subprocess.check_output(
159 [self.git_command, "worktree", "add", "--detach",
160 git_worktree_path, revision], cwd=self.repo_path,
161 stderr=subprocess.STDOUT
162 )
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100163
Xiaofei Baibca03e52021-09-09 09:42:37 +0000164 return git_worktree_path
165
166 def _build_libraries(self, git_worktree_path):
167 """Build libraries in the specified worktree."""
168
169 my_environment = os.environ.copy()
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100170 try:
171 subprocess.check_output(
172 self.make_command, env=my_environment, shell=True,
173 cwd=git_worktree_path, stderr=subprocess.STDOUT,
174 )
175 except subprocess.CalledProcessError as e:
176 self._handle_called_process_error(e, git_worktree_path)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000177
178 def _gen_code_size_csv(self, revision, git_worktree_path):
179 """Generate code size csv file."""
180
Yanray Wang369cd962023-05-24 17:13:29 +0800181 csv_fname = revision + self.fname_suffix + ".csv"
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000182 if revision == "current":
183 print("Measuring code size in current work directory.")
184 else:
185 print("Measuring code size for", revision)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000186 result = subprocess.check_output(
187 ["size library/*.o"], cwd=git_worktree_path, shell=True
188 )
189 size_text = result.decode()
190 csv_file = open(os.path.join(self.csv_dir, csv_fname), "w")
191 for line in size_text.splitlines()[1:]:
192 data = line.split()
193 csv_file.write("{}, {}\n".format(data[5], data[3]))
194
195 def _remove_worktree(self, git_worktree_path):
196 """Remove temporary worktree."""
197 if git_worktree_path != self.repo_path:
198 print("Removing temporary worktree", git_worktree_path)
199 subprocess.check_output(
200 [self.git_command, "worktree", "remove", "--force",
201 git_worktree_path], cwd=self.repo_path,
202 stderr=subprocess.STDOUT
203 )
204
205 def _get_code_size_for_rev(self, revision):
206 """Generate code size csv file for the specified git revision."""
207
208 # Check if the corresponding record exists
Yanray Wang369cd962023-05-24 17:13:29 +0800209 csv_fname = revision + self.fname_suffix + ".csv"
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000210 if (revision != "current") and \
Xiaofei Baibca03e52021-09-09 09:42:37 +0000211 os.path.exists(os.path.join(self.csv_dir, csv_fname)):
212 print("Code size csv file for", revision, "already exists.")
213 else:
214 git_worktree_path = self._create_git_worktree(revision)
215 self._build_libraries(git_worktree_path)
216 self._gen_code_size_csv(revision, git_worktree_path)
217 self._remove_worktree(git_worktree_path)
218
219 def compare_code_size(self):
220 """Generate results of the size changes between two revisions,
221 old and new. Measured code size results of these two revisions
Xiaofei Bai2400b502021-10-21 12:22:58 +0000222 must be available."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000223
Yanray Wang369cd962023-05-24 17:13:29 +0800224 old_file = open(os.path.join(self.csv_dir, self.old_rev +
225 self.fname_suffix + ".csv"), "r")
226 new_file = open(os.path.join(self.csv_dir, self.new_rev +
227 self.fname_suffix + ".csv"), "r")
228 res_file = open(os.path.join(self.result_dir, "compare-" +
229 self.old_rev + "-" + self.new_rev +
230 self.fname_suffix +
231 ".csv"), "w")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000232
Xiaofei Baibca03e52021-09-09 09:42:37 +0000233 res_file.write("file_name, this_size, old_size, change, change %\n")
Shaun Case8b0ecbc2021-12-20 21:14:10 -0800234 print("Generating comparison results.")
Xiaofei Baibca03e52021-09-09 09:42:37 +0000235
236 old_ds = {}
Yanray Wanga3841ab2023-05-24 18:33:08 +0800237 for line in old_file.readlines():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000238 cols = line.split(", ")
239 fname = cols[0]
240 size = int(cols[1])
241 if size != 0:
242 old_ds[fname] = size
243
244 new_ds = {}
Yanray Wanga3841ab2023-05-24 18:33:08 +0800245 for line in new_file.readlines():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000246 cols = line.split(", ")
247 fname = cols[0]
248 size = int(cols[1])
249 new_ds[fname] = size
250
251 for fname in new_ds:
252 this_size = new_ds[fname]
253 if fname in old_ds:
254 old_size = old_ds[fname]
255 change = this_size - old_size
256 change_pct = change / old_size
257 res_file.write("{}, {}, {}, {}, {:.2%}\n".format(fname, \
258 this_size, old_size, change, float(change_pct)))
259 else:
260 res_file.write("{}, {}\n".format(fname, this_size))
Xiaofei Bai2400b502021-10-21 12:22:58 +0000261 return 0
Xiaofei Baibca03e52021-09-09 09:42:37 +0000262
263 def get_comparision_results(self):
264 """Compare size of library/*.o between self.old_rev and self.new_rev,
265 and generate the result file."""
Gilles Peskined9071e72022-09-18 21:17:09 +0200266 build_tree.check_repo_path()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000267 self._get_code_size_for_rev(self.old_rev)
268 self._get_code_size_for_rev(self.new_rev)
269 return self.compare_code_size()
270
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100271 def _handle_called_process_error(self, e: subprocess.CalledProcessError,
272 git_worktree_path):
273 """Handle a CalledProcessError and quit the program gracefully.
274 Remove any extra worktrees so that the script may be called again."""
275
276 # Tell the user what went wrong
277 print("The following command: {} failed and exited with code {}"
278 .format(e.cmd, e.returncode))
279 print("Process output:\n {}".format(str(e.output, "utf-8")))
280
281 # Quit gracefully by removing the existing worktree
282 self._remove_worktree(git_worktree_path)
283 sys.exit(-1)
284
Xiaofei Bai2400b502021-10-21 12:22:58 +0000285def main():
Yanray Wang502c54f2023-05-31 11:41:36 +0800286 parser = argparse.ArgumentParser(description=(__doc__))
287 group_required = parser.add_argument_group(
288 'required arguments',
289 'required arguments to parse for running ' + os.path.basename(__file__))
290 group_required.add_argument(
291 "-o", "--old-rev", type=str, required=True,
292 help="old revision for comparison.")
293
294 group_optional = parser.add_argument_group(
295 'optional arguments',
296 'optional arguments to parse for running ' + os.path.basename(__file__))
297 group_optional.add_argument(
Xiaofei Baibca03e52021-09-09 09:42:37 +0000298 "-r", "--result-dir", type=str, default="comparison",
299 help="directory where comparison result is stored, \
Yanray Wang502c54f2023-05-31 11:41:36 +0800300 default is comparison")
301 group_optional.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000302 "-n", "--new-rev", type=str, default=None,
303 help="new revision for comparison, default is the current work \
Yanray Wang502c54f2023-05-31 11:41:36 +0800304 directory, including uncommitted changes.")
305 group_optional.add_argument(
Yanray Wang23bd5322023-05-24 11:03:59 +0800306 "-a", "--arch", type=str, default=detect_arch(),
307 choices=list(map(lambda s: s.value, SupportedArch)),
308 help="specify architecture for code size comparison, default is the\
Yanray Wang502c54f2023-05-31 11:41:36 +0800309 host architecture.")
310 group_optional.add_argument(
Yanray Wang6a862582023-05-24 12:24:38 +0800311 "-c", "--config", type=str, default=SupportedConfig.DEFAULT.value,
312 choices=list(map(lambda s: s.value, SupportedConfig)),
313 help="specify configuration type for code size comparison,\
Yanray Wang502c54f2023-05-31 11:41:36 +0800314 default is the current MbedTLS configuration.")
Xiaofei Baibca03e52021-09-09 09:42:37 +0000315 comp_args = parser.parse_args()
316
317 if os.path.isfile(comp_args.result_dir):
318 print("Error: {} is not a directory".format(comp_args.result_dir))
319 parser.exit()
320
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000321 validate_res = CodeSizeComparison.validate_revision(comp_args.old_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000322 old_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai2400b502021-10-21 12:22:58 +0000323
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000324 if comp_args.new_rev is not None:
325 validate_res = CodeSizeComparison.validate_revision(comp_args.new_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000326 new_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000327 else:
328 new_revision = "current"
Xiaofei Bai2400b502021-10-21 12:22:58 +0000329
Yanray Wang21f17442023-06-01 11:29:06 +0800330 code_size_info = CodeSizeInfo(comp_args.arch, comp_args.config,
331 detect_arch())
Yanray Wangaba71582023-05-29 16:45:56 +0800332 print("Measure code size for architecture: {}, configuration: {}"
333 .format(code_size_info.arch, code_size_info.config))
Xiaofei Baibca03e52021-09-09 09:42:37 +0000334 result_dir = comp_args.result_dir
Yanray Wang6a862582023-05-24 12:24:38 +0800335 size_compare = CodeSizeComparison(old_revision, new_revision, result_dir,
336 code_size_info)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000337 return_code = size_compare.get_comparision_results()
338 sys.exit(return_code)
339
340
341if __name__ == "__main__":
Xiaofei Bai2400b502021-10-21 12:22:58 +0000342 main()