blob: b8f29422d400b3c4f2a39484258c56c6dd88b5a7 [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
Yanray Wangc18cd892023-05-31 11:08:04 +080074 SupportedArchConfig = [
75 "-a " + SupportedArch.AARCH64.value + " -c " + SupportedConfig.DEFAULT.value,
76 "-a " + SupportedArch.AARCH32.value + " -c " + SupportedConfig.DEFAULT.value,
77 "-a " + SupportedArch.X86_64.value + " -c " + SupportedConfig.DEFAULT.value,
78 "-a " + SupportedArch.X86.value + " -c " + SupportedConfig.DEFAULT.value,
79 "-a " + SupportedArch.ARMV8_M.value + " -c " + SupportedConfig.TFM_MEDIUM.value,
80 ]
81
Yanray Wang6a862582023-05-24 12:24:38 +080082 def __init__(self, arch: str, config: str) -> None:
83 """
84 arch: architecture to measure code size on.
85 config: configuration type to measure code size with.
86 make_command: command to build library (Inferred from arch and config).
87 """
88 self.arch = arch
89 self.config = config
90 self.make_command = self.set_make_command()
91
92 def set_make_command(self) -> str:
93 """Infer build command based on architecture and configuration."""
94
95 if self.config == SupportedConfig.DEFAULT.value:
96 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:
105 print("Unsupported architecture: {} and configurations: {}"
106 .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 Wang6a862582023-05-24 12:24:38 +0800110 sys.exit(1)
111
112
Xiaofei Baibca03e52021-09-09 09:42:37 +0000113class CodeSizeComparison:
Xiaofei Bai2400b502021-10-21 12:22:58 +0000114 """Compare code size between two Git revisions."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000115
Yanray Wang6a862582023-05-24 12:24:38 +0800116 def __init__(self, old_revision, new_revision, result_dir, code_size_info):
Xiaofei Baibca03e52021-09-09 09:42:37 +0000117 """
Yanray Wang6a862582023-05-24 12:24:38 +0800118 old_revision: revision to compare against.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000119 new_revision:
Yanray Wang6a862582023-05-24 12:24:38 +0800120 result_dir: directory for comparison result.
121 code_size_info: an object containing information to build library.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000122 """
123 self.repo_path = "."
124 self.result_dir = os.path.abspath(result_dir)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000125 os.makedirs(self.result_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000126
127 self.csv_dir = os.path.abspath("code_size_records/")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000128 os.makedirs(self.csv_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000129
130 self.old_rev = old_revision
131 self.new_rev = new_revision
132 self.git_command = "git"
Yanray Wang6a862582023-05-24 12:24:38 +0800133 self.make_command = code_size_info.make_command
Yanray Wang369cd962023-05-24 17:13:29 +0800134 self.fname_suffix = "-" + code_size_info.arch + "-" +\
135 code_size_info.config
Xiaofei Baibca03e52021-09-09 09:42:37 +0000136
137 @staticmethod
Xiaofei Bai2400b502021-10-21 12:22:58 +0000138 def validate_revision(revision):
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000139 result = subprocess.check_output(["git", "rev-parse", "--verify",
140 revision + "^{commit}"], shell=False)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000141 return result
Xiaofei Bai2400b502021-10-21 12:22:58 +0000142
Xiaofei Baibca03e52021-09-09 09:42:37 +0000143 def _create_git_worktree(self, revision):
144 """Make a separate worktree for revision.
145 Do not modify the current worktree."""
146
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000147 if revision == "current":
Xiaofei Baibca03e52021-09-09 09:42:37 +0000148 print("Using current work directory.")
149 git_worktree_path = self.repo_path
150 else:
151 print("Creating git worktree for", revision)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000152 git_worktree_path = os.path.join(self.repo_path, "temp-" + revision)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000153 subprocess.check_output(
154 [self.git_command, "worktree", "add", "--detach",
155 git_worktree_path, revision], cwd=self.repo_path,
156 stderr=subprocess.STDOUT
157 )
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100158
Xiaofei Baibca03e52021-09-09 09:42:37 +0000159 return git_worktree_path
160
161 def _build_libraries(self, git_worktree_path):
162 """Build libraries in the specified worktree."""
163
164 my_environment = os.environ.copy()
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100165 try:
166 subprocess.check_output(
167 self.make_command, env=my_environment, shell=True,
168 cwd=git_worktree_path, stderr=subprocess.STDOUT,
169 )
170 except subprocess.CalledProcessError as e:
171 self._handle_called_process_error(e, git_worktree_path)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000172
173 def _gen_code_size_csv(self, revision, git_worktree_path):
174 """Generate code size csv file."""
175
Yanray Wang369cd962023-05-24 17:13:29 +0800176 csv_fname = revision + self.fname_suffix + ".csv"
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000177 if revision == "current":
178 print("Measuring code size in current work directory.")
179 else:
180 print("Measuring code size for", revision)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000181 result = subprocess.check_output(
182 ["size library/*.o"], cwd=git_worktree_path, shell=True
183 )
184 size_text = result.decode()
185 csv_file = open(os.path.join(self.csv_dir, csv_fname), "w")
186 for line in size_text.splitlines()[1:]:
187 data = line.split()
188 csv_file.write("{}, {}\n".format(data[5], data[3]))
189
190 def _remove_worktree(self, git_worktree_path):
191 """Remove temporary worktree."""
192 if git_worktree_path != self.repo_path:
193 print("Removing temporary worktree", git_worktree_path)
194 subprocess.check_output(
195 [self.git_command, "worktree", "remove", "--force",
196 git_worktree_path], cwd=self.repo_path,
197 stderr=subprocess.STDOUT
198 )
199
200 def _get_code_size_for_rev(self, revision):
201 """Generate code size csv file for the specified git revision."""
202
203 # Check if the corresponding record exists
Yanray Wang369cd962023-05-24 17:13:29 +0800204 csv_fname = revision + self.fname_suffix + ".csv"
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000205 if (revision != "current") and \
Xiaofei Baibca03e52021-09-09 09:42:37 +0000206 os.path.exists(os.path.join(self.csv_dir, csv_fname)):
207 print("Code size csv file for", revision, "already exists.")
208 else:
209 git_worktree_path = self._create_git_worktree(revision)
210 self._build_libraries(git_worktree_path)
211 self._gen_code_size_csv(revision, git_worktree_path)
212 self._remove_worktree(git_worktree_path)
213
214 def compare_code_size(self):
215 """Generate results of the size changes between two revisions,
216 old and new. Measured code size results of these two revisions
Xiaofei Bai2400b502021-10-21 12:22:58 +0000217 must be available."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000218
Yanray Wang369cd962023-05-24 17:13:29 +0800219 old_file = open(os.path.join(self.csv_dir, self.old_rev +
220 self.fname_suffix + ".csv"), "r")
221 new_file = open(os.path.join(self.csv_dir, self.new_rev +
222 self.fname_suffix + ".csv"), "r")
223 res_file = open(os.path.join(self.result_dir, "compare-" +
224 self.old_rev + "-" + self.new_rev +
225 self.fname_suffix +
226 ".csv"), "w")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000227
Xiaofei Baibca03e52021-09-09 09:42:37 +0000228 res_file.write("file_name, this_size, old_size, change, change %\n")
Shaun Case8b0ecbc2021-12-20 21:14:10 -0800229 print("Generating comparison results.")
Xiaofei Baibca03e52021-09-09 09:42:37 +0000230
231 old_ds = {}
Yanray Wanga3841ab2023-05-24 18:33:08 +0800232 for line in old_file.readlines():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000233 cols = line.split(", ")
234 fname = cols[0]
235 size = int(cols[1])
236 if size != 0:
237 old_ds[fname] = size
238
239 new_ds = {}
Yanray Wanga3841ab2023-05-24 18:33:08 +0800240 for line in new_file.readlines():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000241 cols = line.split(", ")
242 fname = cols[0]
243 size = int(cols[1])
244 new_ds[fname] = size
245
246 for fname in new_ds:
247 this_size = new_ds[fname]
248 if fname in old_ds:
249 old_size = old_ds[fname]
250 change = this_size - old_size
251 change_pct = change / old_size
252 res_file.write("{}, {}, {}, {}, {:.2%}\n".format(fname, \
253 this_size, old_size, change, float(change_pct)))
254 else:
255 res_file.write("{}, {}\n".format(fname, this_size))
Xiaofei Bai2400b502021-10-21 12:22:58 +0000256 return 0
Xiaofei Baibca03e52021-09-09 09:42:37 +0000257
258 def get_comparision_results(self):
259 """Compare size of library/*.o between self.old_rev and self.new_rev,
260 and generate the result file."""
Gilles Peskined9071e72022-09-18 21:17:09 +0200261 build_tree.check_repo_path()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000262 self._get_code_size_for_rev(self.old_rev)
263 self._get_code_size_for_rev(self.new_rev)
264 return self.compare_code_size()
265
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100266 def _handle_called_process_error(self, e: subprocess.CalledProcessError,
267 git_worktree_path):
268 """Handle a CalledProcessError and quit the program gracefully.
269 Remove any extra worktrees so that the script may be called again."""
270
271 # Tell the user what went wrong
272 print("The following command: {} failed and exited with code {}"
273 .format(e.cmd, e.returncode))
274 print("Process output:\n {}".format(str(e.output, "utf-8")))
275
276 # Quit gracefully by removing the existing worktree
277 self._remove_worktree(git_worktree_path)
278 sys.exit(-1)
279
Xiaofei Bai2400b502021-10-21 12:22:58 +0000280def main():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000281 parser = argparse.ArgumentParser(
282 description=(
283 """This script is for comparing the size of the library files
284 from two different Git revisions within an Mbed TLS repository.
285 The results of the comparison is formatted as csv, and stored at
286 a configurable location.
287 Note: must be run from Mbed TLS root."""
288 )
289 )
290 parser.add_argument(
291 "-r", "--result-dir", type=str, default="comparison",
292 help="directory where comparison result is stored, \
293 default is comparison",
294 )
295 parser.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000296 "-o", "--old-rev", type=str, help="old revision for comparison.",
Xiaofei Baibca03e52021-09-09 09:42:37 +0000297 required=True,
298 )
299 parser.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000300 "-n", "--new-rev", type=str, default=None,
301 help="new revision for comparison, default is the current work \
Shaun Case8b0ecbc2021-12-20 21:14:10 -0800302 directory, including uncommitted changes."
Xiaofei Baibca03e52021-09-09 09:42:37 +0000303 )
Yanray Wang23bd5322023-05-24 11:03:59 +0800304 parser.add_argument(
305 "-a", "--arch", type=str, default=detect_arch(),
306 choices=list(map(lambda s: s.value, SupportedArch)),
307 help="specify architecture for code size comparison, default is the\
308 host architecture."
309 )
Yanray Wang6a862582023-05-24 12:24:38 +0800310 parser.add_argument(
311 "-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,\
314 default is the current MbedTLS configuration."
315 )
Xiaofei Baibca03e52021-09-09 09:42:37 +0000316 comp_args = parser.parse_args()
317
318 if os.path.isfile(comp_args.result_dir):
319 print("Error: {} is not a directory".format(comp_args.result_dir))
320 parser.exit()
321
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000322 validate_res = CodeSizeComparison.validate_revision(comp_args.old_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000323 old_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai2400b502021-10-21 12:22:58 +0000324
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000325 if comp_args.new_rev is not None:
326 validate_res = CodeSizeComparison.validate_revision(comp_args.new_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000327 new_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000328 else:
329 new_revision = "current"
Xiaofei Bai2400b502021-10-21 12:22:58 +0000330
Yanray Wang6a862582023-05-24 12:24:38 +0800331 code_size_info = CodeSizeInfo(comp_args.arch, comp_args.config)
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()