blob: e61236ad3cbb38a0d3973531964135212983adb2 [file] [log] [blame]
Xiaofei Baibca03e52021-09-09 09:42:37 +00001#!/usr/bin/env python3
2
3"""
4Purpose
5
6This script is for comparing the size of the library files from two
7different Git revisions within an Mbed TLS repository.
8The results of the comparison is formatted as csv and stored at a
9configurable location.
10Note: must be run from Mbed TLS root.
11"""
12
13# Copyright The Mbed TLS Contributors
14# SPDX-License-Identifier: Apache-2.0
15#
16# Licensed under the Apache License, Version 2.0 (the "License"); you may
17# not use this file except in compliance with the License.
18# You may obtain a copy of the License at
19#
20# http://www.apache.org/licenses/LICENSE-2.0
21#
22# Unless required by applicable law or agreed to in writing, software
23# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
24# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25# See the License for the specific language governing permissions and
26# limitations under the License.
27
28import argparse
29import os
30import subprocess
31import sys
Yanray Wang23bd5322023-05-24 11:03:59 +080032from enum import Enum
Xiaofei Baibca03e52021-09-09 09:42:37 +000033
Gilles Peskined9071e72022-09-18 21:17:09 +020034from mbedtls_dev import build_tree
35
Yanray Wang23bd5322023-05-24 11:03:59 +080036class SupportedArch(Enum):
37 """Supported architecture for code size measurement."""
38 AARCH64 = 'aarch64'
39 AARCH32 = 'aarch32'
Yanray Wangaba71582023-05-29 16:45:56 +080040 ARMV8_M = 'armv8-m'
Yanray Wang23bd5322023-05-24 11:03:59 +080041 X86_64 = 'x86_64'
42 X86 = 'x86'
43
Yanray Wang6a862582023-05-24 12:24:38 +080044CONFIG_TFM_MEDIUM_MBEDCRYPTO_H = "../configs/tfm_mbedcrypto_config_profile_medium.h"
45CONFIG_TFM_MEDIUM_PSA_CRYPTO_H = "../configs/crypto_config_profile_medium.h"
46class SupportedConfig(Enum):
47 """Supported configuration for code size measurement."""
48 DEFAULT = 'default'
49 TFM_MEDIUM = 'tfm-medium'
50
Yanray Wang23bd5322023-05-24 11:03:59 +080051DETECT_ARCH_CMD = "cc -dM -E - < /dev/null"
52def detect_arch() -> str:
53 """Auto-detect host architecture."""
54 cc_output = subprocess.check_output(DETECT_ARCH_CMD, shell=True).decode()
55 if "__aarch64__" in cc_output:
56 return SupportedArch.AARCH64.value
57 if "__arm__" in cc_output:
58 return SupportedArch.AARCH32.value
59 if "__x86_64__" in cc_output:
60 return SupportedArch.X86_64.value
61 if "__x86__" in cc_output:
62 return SupportedArch.X86.value
63 else:
64 print("Unknown host architecture, cannot auto-detect arch.")
65 sys.exit(1)
Gilles Peskined9071e72022-09-18 21:17:09 +020066
Yanray Wang6a862582023-05-24 12:24:38 +080067class CodeSizeInfo: # pylint: disable=too-few-public-methods
68 """Gather information used to measure code size.
69
70 It collects information about architecture, configuration in order to
71 infer build command for code size measurement.
72 """
73
74 def __init__(self, arch: str, config: str) -> None:
75 """
76 arch: architecture to measure code size on.
77 config: configuration type to measure code size with.
78 make_command: command to build library (Inferred from arch and config).
79 """
80 self.arch = arch
81 self.config = config
82 self.make_command = self.set_make_command()
83
84 def set_make_command(self) -> str:
85 """Infer build command based on architecture and configuration."""
86
87 if self.config == SupportedConfig.DEFAULT.value:
88 return 'make -j lib CFLAGS=\'-Os \' '
Yanray Wangaba71582023-05-29 16:45:56 +080089 elif self.arch == SupportedArch.ARMV8_M.value and \
Yanray Wang6a862582023-05-24 12:24:38 +080090 self.config == SupportedConfig.TFM_MEDIUM.value:
91 return \
Yanray Wang60430bd2023-05-29 14:48:18 +080092 'make -j lib CC=armclang \
Yanray Wang6a862582023-05-24 12:24:38 +080093 CFLAGS=\'--target=arm-arm-none-eabi -mcpu=cortex-m33 -Os \
94 -DMBEDTLS_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_MBEDCRYPTO_H + '\\\" \
95 -DMBEDTLS_PSA_CRYPTO_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_PSA_CRYPTO_H + '\\\" \''
96 else:
97 print("Unsupported architecture: {} and configurations: {}"
98 .format(self.arch, self.config))
99 sys.exit(1)
100
101
Xiaofei Baibca03e52021-09-09 09:42:37 +0000102class CodeSizeComparison:
Xiaofei Bai2400b502021-10-21 12:22:58 +0000103 """Compare code size between two Git revisions."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000104
Yanray Wang6a862582023-05-24 12:24:38 +0800105 def __init__(self, old_revision, new_revision, result_dir, code_size_info):
Xiaofei Baibca03e52021-09-09 09:42:37 +0000106 """
Yanray Wang6a862582023-05-24 12:24:38 +0800107 old_revision: revision to compare against.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000108 new_revision:
Yanray Wang6a862582023-05-24 12:24:38 +0800109 result_dir: directory for comparison result.
110 code_size_info: an object containing information to build library.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000111 """
112 self.repo_path = "."
113 self.result_dir = os.path.abspath(result_dir)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000114 os.makedirs(self.result_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000115
116 self.csv_dir = os.path.abspath("code_size_records/")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000117 os.makedirs(self.csv_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000118
119 self.old_rev = old_revision
120 self.new_rev = new_revision
121 self.git_command = "git"
Yanray Wang6a862582023-05-24 12:24:38 +0800122 self.make_command = code_size_info.make_command
Yanray Wang369cd962023-05-24 17:13:29 +0800123 self.fname_suffix = "-" + code_size_info.arch + "-" +\
124 code_size_info.config
Xiaofei Baibca03e52021-09-09 09:42:37 +0000125
126 @staticmethod
Xiaofei Bai2400b502021-10-21 12:22:58 +0000127 def validate_revision(revision):
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000128 result = subprocess.check_output(["git", "rev-parse", "--verify",
129 revision + "^{commit}"], shell=False)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000130 return result
Xiaofei Bai2400b502021-10-21 12:22:58 +0000131
Xiaofei Baibca03e52021-09-09 09:42:37 +0000132 def _create_git_worktree(self, revision):
133 """Make a separate worktree for revision.
134 Do not modify the current worktree."""
135
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000136 if revision == "current":
Xiaofei Baibca03e52021-09-09 09:42:37 +0000137 print("Using current work directory.")
138 git_worktree_path = self.repo_path
139 else:
140 print("Creating git worktree for", revision)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000141 git_worktree_path = os.path.join(self.repo_path, "temp-" + revision)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000142 subprocess.check_output(
143 [self.git_command, "worktree", "add", "--detach",
144 git_worktree_path, revision], cwd=self.repo_path,
145 stderr=subprocess.STDOUT
146 )
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100147
Xiaofei Baibca03e52021-09-09 09:42:37 +0000148 return git_worktree_path
149
150 def _build_libraries(self, git_worktree_path):
151 """Build libraries in the specified worktree."""
152
153 my_environment = os.environ.copy()
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100154 try:
155 subprocess.check_output(
156 self.make_command, env=my_environment, shell=True,
157 cwd=git_worktree_path, stderr=subprocess.STDOUT,
158 )
159 except subprocess.CalledProcessError as e:
160 self._handle_called_process_error(e, git_worktree_path)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000161
162 def _gen_code_size_csv(self, revision, git_worktree_path):
163 """Generate code size csv file."""
164
Yanray Wang369cd962023-05-24 17:13:29 +0800165 csv_fname = revision + self.fname_suffix + ".csv"
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000166 if revision == "current":
167 print("Measuring code size in current work directory.")
168 else:
169 print("Measuring code size for", revision)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000170 result = subprocess.check_output(
171 ["size library/*.o"], cwd=git_worktree_path, shell=True
172 )
173 size_text = result.decode()
174 csv_file = open(os.path.join(self.csv_dir, csv_fname), "w")
175 for line in size_text.splitlines()[1:]:
176 data = line.split()
177 csv_file.write("{}, {}\n".format(data[5], data[3]))
178
179 def _remove_worktree(self, git_worktree_path):
180 """Remove temporary worktree."""
181 if git_worktree_path != self.repo_path:
182 print("Removing temporary worktree", git_worktree_path)
183 subprocess.check_output(
184 [self.git_command, "worktree", "remove", "--force",
185 git_worktree_path], cwd=self.repo_path,
186 stderr=subprocess.STDOUT
187 )
188
189 def _get_code_size_for_rev(self, revision):
190 """Generate code size csv file for the specified git revision."""
191
192 # Check if the corresponding record exists
Yanray Wang369cd962023-05-24 17:13:29 +0800193 csv_fname = revision + self.fname_suffix + ".csv"
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000194 if (revision != "current") and \
Xiaofei Baibca03e52021-09-09 09:42:37 +0000195 os.path.exists(os.path.join(self.csv_dir, csv_fname)):
196 print("Code size csv file for", revision, "already exists.")
197 else:
198 git_worktree_path = self._create_git_worktree(revision)
199 self._build_libraries(git_worktree_path)
200 self._gen_code_size_csv(revision, git_worktree_path)
201 self._remove_worktree(git_worktree_path)
202
203 def compare_code_size(self):
204 """Generate results of the size changes between two revisions,
205 old and new. Measured code size results of these two revisions
Xiaofei Bai2400b502021-10-21 12:22:58 +0000206 must be available."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000207
Yanray Wang369cd962023-05-24 17:13:29 +0800208 old_file = open(os.path.join(self.csv_dir, self.old_rev +
209 self.fname_suffix + ".csv"), "r")
210 new_file = open(os.path.join(self.csv_dir, self.new_rev +
211 self.fname_suffix + ".csv"), "r")
212 res_file = open(os.path.join(self.result_dir, "compare-" +
213 self.old_rev + "-" + self.new_rev +
214 self.fname_suffix +
215 ".csv"), "w")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000216
Xiaofei Baibca03e52021-09-09 09:42:37 +0000217 res_file.write("file_name, this_size, old_size, change, change %\n")
Shaun Case8b0ecbc2021-12-20 21:14:10 -0800218 print("Generating comparison results.")
Xiaofei Baibca03e52021-09-09 09:42:37 +0000219
220 old_ds = {}
Yanray Wanga3841ab2023-05-24 18:33:08 +0800221 for line in old_file.readlines():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000222 cols = line.split(", ")
223 fname = cols[0]
224 size = int(cols[1])
225 if size != 0:
226 old_ds[fname] = size
227
228 new_ds = {}
Yanray Wanga3841ab2023-05-24 18:33:08 +0800229 for line in new_file.readlines():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000230 cols = line.split(", ")
231 fname = cols[0]
232 size = int(cols[1])
233 new_ds[fname] = size
234
235 for fname in new_ds:
236 this_size = new_ds[fname]
237 if fname in old_ds:
238 old_size = old_ds[fname]
239 change = this_size - old_size
240 change_pct = change / old_size
241 res_file.write("{}, {}, {}, {}, {:.2%}\n".format(fname, \
242 this_size, old_size, change, float(change_pct)))
243 else:
244 res_file.write("{}, {}\n".format(fname, this_size))
Xiaofei Bai2400b502021-10-21 12:22:58 +0000245 return 0
Xiaofei Baibca03e52021-09-09 09:42:37 +0000246
247 def get_comparision_results(self):
248 """Compare size of library/*.o between self.old_rev and self.new_rev,
249 and generate the result file."""
Gilles Peskined9071e72022-09-18 21:17:09 +0200250 build_tree.check_repo_path()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000251 self._get_code_size_for_rev(self.old_rev)
252 self._get_code_size_for_rev(self.new_rev)
253 return self.compare_code_size()
254
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100255 def _handle_called_process_error(self, e: subprocess.CalledProcessError,
256 git_worktree_path):
257 """Handle a CalledProcessError and quit the program gracefully.
258 Remove any extra worktrees so that the script may be called again."""
259
260 # Tell the user what went wrong
261 print("The following command: {} failed and exited with code {}"
262 .format(e.cmd, e.returncode))
263 print("Process output:\n {}".format(str(e.output, "utf-8")))
264
265 # Quit gracefully by removing the existing worktree
266 self._remove_worktree(git_worktree_path)
267 sys.exit(-1)
268
Xiaofei Bai2400b502021-10-21 12:22:58 +0000269def main():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000270 parser = argparse.ArgumentParser(
271 description=(
272 """This script is for comparing the size of the library files
273 from two different Git revisions within an Mbed TLS repository.
274 The results of the comparison is formatted as csv, and stored at
275 a configurable location.
276 Note: must be run from Mbed TLS root."""
277 )
278 )
279 parser.add_argument(
280 "-r", "--result-dir", type=str, default="comparison",
281 help="directory where comparison result is stored, \
282 default is comparison",
283 )
284 parser.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000285 "-o", "--old-rev", type=str, help="old revision for comparison.",
Xiaofei Baibca03e52021-09-09 09:42:37 +0000286 required=True,
287 )
288 parser.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000289 "-n", "--new-rev", type=str, default=None,
290 help="new revision for comparison, default is the current work \
Shaun Case8b0ecbc2021-12-20 21:14:10 -0800291 directory, including uncommitted changes."
Xiaofei Baibca03e52021-09-09 09:42:37 +0000292 )
Yanray Wang23bd5322023-05-24 11:03:59 +0800293 parser.add_argument(
294 "-a", "--arch", type=str, default=detect_arch(),
295 choices=list(map(lambda s: s.value, SupportedArch)),
296 help="specify architecture for code size comparison, default is the\
297 host architecture."
298 )
Yanray Wang6a862582023-05-24 12:24:38 +0800299 parser.add_argument(
300 "-c", "--config", type=str, default=SupportedConfig.DEFAULT.value,
301 choices=list(map(lambda s: s.value, SupportedConfig)),
302 help="specify configuration type for code size comparison,\
303 default is the current MbedTLS configuration."
304 )
Xiaofei Baibca03e52021-09-09 09:42:37 +0000305 comp_args = parser.parse_args()
306
307 if os.path.isfile(comp_args.result_dir):
308 print("Error: {} is not a directory".format(comp_args.result_dir))
309 parser.exit()
310
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000311 validate_res = CodeSizeComparison.validate_revision(comp_args.old_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000312 old_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai2400b502021-10-21 12:22:58 +0000313
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000314 if comp_args.new_rev is not None:
315 validate_res = CodeSizeComparison.validate_revision(comp_args.new_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000316 new_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000317 else:
318 new_revision = "current"
Xiaofei Bai2400b502021-10-21 12:22:58 +0000319
Yanray Wang6a862582023-05-24 12:24:38 +0800320 code_size_info = CodeSizeInfo(comp_args.arch, comp_args.config)
Yanray Wangaba71582023-05-29 16:45:56 +0800321 print("Measure code size for architecture: {}, configuration: {}"
322 .format(code_size_info.arch, code_size_info.config))
Xiaofei Baibca03e52021-09-09 09:42:37 +0000323 result_dir = comp_args.result_dir
Yanray Wang6a862582023-05-24 12:24:38 +0800324 size_compare = CodeSizeComparison(old_revision, new_revision, result_dir,
325 code_size_info)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000326 return_code = size_compare.get_comparision_results()
327 sys.exit(return_code)
328
329
330if __name__ == "__main__":
Xiaofei Bai2400b502021-10-21 12:22:58 +0000331 main()