blob: 86facb964ad0582ba8d0490a0f91d57ac245a414 [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'
40 X86_64 = 'x86_64'
41 X86 = 'x86'
42
Yanray Wang6a862582023-05-24 12:24:38 +080043CONFIG_TFM_MEDIUM_MBEDCRYPTO_H = "../configs/tfm_mbedcrypto_config_profile_medium.h"
44CONFIG_TFM_MEDIUM_PSA_CRYPTO_H = "../configs/crypto_config_profile_medium.h"
45class SupportedConfig(Enum):
46 """Supported configuration for code size measurement."""
47 DEFAULT = 'default'
48 TFM_MEDIUM = 'tfm-medium'
49
Yanray Wang23bd5322023-05-24 11:03:59 +080050DETECT_ARCH_CMD = "cc -dM -E - < /dev/null"
51def detect_arch() -> str:
52 """Auto-detect host architecture."""
53 cc_output = subprocess.check_output(DETECT_ARCH_CMD, shell=True).decode()
54 if "__aarch64__" in cc_output:
55 return SupportedArch.AARCH64.value
56 if "__arm__" in cc_output:
57 return SupportedArch.AARCH32.value
58 if "__x86_64__" in cc_output:
59 return SupportedArch.X86_64.value
60 if "__x86__" in cc_output:
61 return SupportedArch.X86.value
62 else:
63 print("Unknown host architecture, cannot auto-detect arch.")
64 sys.exit(1)
Gilles Peskined9071e72022-09-18 21:17:09 +020065
Yanray Wang6a862582023-05-24 12:24:38 +080066class CodeSizeInfo: # pylint: disable=too-few-public-methods
67 """Gather information used to measure code size.
68
69 It collects information about architecture, configuration in order to
70 infer build command for code size measurement.
71 """
72
73 def __init__(self, arch: str, config: str) -> None:
74 """
75 arch: architecture to measure code size on.
76 config: configuration type to measure code size with.
77 make_command: command to build library (Inferred from arch and config).
78 """
79 self.arch = arch
80 self.config = config
81 self.make_command = self.set_make_command()
82
83 def set_make_command(self) -> str:
84 """Infer build command based on architecture and configuration."""
85
86 if self.config == SupportedConfig.DEFAULT.value:
87 return 'make -j lib CFLAGS=\'-Os \' '
88 elif self.arch == SupportedArch.AARCH32.value and \
89 self.config == SupportedConfig.TFM_MEDIUM.value:
90 return \
Yanray Wang60430bd2023-05-29 14:48:18 +080091 'make -j lib CC=armclang \
Yanray Wang6a862582023-05-24 12:24:38 +080092 CFLAGS=\'--target=arm-arm-none-eabi -mcpu=cortex-m33 -Os \
93 -DMBEDTLS_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_MBEDCRYPTO_H + '\\\" \
94 -DMBEDTLS_PSA_CRYPTO_CONFIG_FILE=\\\"' + CONFIG_TFM_MEDIUM_PSA_CRYPTO_H + '\\\" \''
95 else:
96 print("Unsupported architecture: {} and configurations: {}"
97 .format(self.arch, self.config))
98 sys.exit(1)
99
100
Xiaofei Baibca03e52021-09-09 09:42:37 +0000101class CodeSizeComparison:
Xiaofei Bai2400b502021-10-21 12:22:58 +0000102 """Compare code size between two Git revisions."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000103
Yanray Wang6a862582023-05-24 12:24:38 +0800104 def __init__(self, old_revision, new_revision, result_dir, code_size_info):
Xiaofei Baibca03e52021-09-09 09:42:37 +0000105 """
Yanray Wang6a862582023-05-24 12:24:38 +0800106 old_revision: revision to compare against.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000107 new_revision:
Yanray Wang6a862582023-05-24 12:24:38 +0800108 result_dir: directory for comparison result.
109 code_size_info: an object containing information to build library.
Xiaofei Baibca03e52021-09-09 09:42:37 +0000110 """
111 self.repo_path = "."
112 self.result_dir = os.path.abspath(result_dir)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000113 os.makedirs(self.result_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000114
115 self.csv_dir = os.path.abspath("code_size_records/")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000116 os.makedirs(self.csv_dir, exist_ok=True)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000117
118 self.old_rev = old_revision
119 self.new_rev = new_revision
120 self.git_command = "git"
Yanray Wang6a862582023-05-24 12:24:38 +0800121 self.make_command = code_size_info.make_command
Yanray Wang369cd962023-05-24 17:13:29 +0800122 self.fname_suffix = "-" + code_size_info.arch + "-" +\
123 code_size_info.config
Xiaofei Baibca03e52021-09-09 09:42:37 +0000124
125 @staticmethod
Xiaofei Bai2400b502021-10-21 12:22:58 +0000126 def validate_revision(revision):
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000127 result = subprocess.check_output(["git", "rev-parse", "--verify",
128 revision + "^{commit}"], shell=False)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000129 return result
Xiaofei Bai2400b502021-10-21 12:22:58 +0000130
Xiaofei Baibca03e52021-09-09 09:42:37 +0000131 def _create_git_worktree(self, revision):
132 """Make a separate worktree for revision.
133 Do not modify the current worktree."""
134
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000135 if revision == "current":
Xiaofei Baibca03e52021-09-09 09:42:37 +0000136 print("Using current work directory.")
137 git_worktree_path = self.repo_path
138 else:
139 print("Creating git worktree for", revision)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000140 git_worktree_path = os.path.join(self.repo_path, "temp-" + revision)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000141 subprocess.check_output(
142 [self.git_command, "worktree", "add", "--detach",
143 git_worktree_path, revision], cwd=self.repo_path,
144 stderr=subprocess.STDOUT
145 )
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100146
Xiaofei Baibca03e52021-09-09 09:42:37 +0000147 return git_worktree_path
148
149 def _build_libraries(self, git_worktree_path):
150 """Build libraries in the specified worktree."""
151
152 my_environment = os.environ.copy()
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100153 try:
154 subprocess.check_output(
155 self.make_command, env=my_environment, shell=True,
156 cwd=git_worktree_path, stderr=subprocess.STDOUT,
157 )
158 except subprocess.CalledProcessError as e:
159 self._handle_called_process_error(e, git_worktree_path)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000160
161 def _gen_code_size_csv(self, revision, git_worktree_path):
162 """Generate code size csv file."""
163
Yanray Wang369cd962023-05-24 17:13:29 +0800164 csv_fname = revision + self.fname_suffix + ".csv"
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000165 if revision == "current":
166 print("Measuring code size in current work directory.")
167 else:
168 print("Measuring code size for", revision)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000169 result = subprocess.check_output(
170 ["size library/*.o"], cwd=git_worktree_path, shell=True
171 )
172 size_text = result.decode()
173 csv_file = open(os.path.join(self.csv_dir, csv_fname), "w")
174 for line in size_text.splitlines()[1:]:
175 data = line.split()
176 csv_file.write("{}, {}\n".format(data[5], data[3]))
177
178 def _remove_worktree(self, git_worktree_path):
179 """Remove temporary worktree."""
180 if git_worktree_path != self.repo_path:
181 print("Removing temporary worktree", git_worktree_path)
182 subprocess.check_output(
183 [self.git_command, "worktree", "remove", "--force",
184 git_worktree_path], cwd=self.repo_path,
185 stderr=subprocess.STDOUT
186 )
187
188 def _get_code_size_for_rev(self, revision):
189 """Generate code size csv file for the specified git revision."""
190
191 # Check if the corresponding record exists
Yanray Wang369cd962023-05-24 17:13:29 +0800192 csv_fname = revision + self.fname_suffix + ".csv"
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000193 if (revision != "current") and \
Xiaofei Baibca03e52021-09-09 09:42:37 +0000194 os.path.exists(os.path.join(self.csv_dir, csv_fname)):
195 print("Code size csv file for", revision, "already exists.")
196 else:
197 git_worktree_path = self._create_git_worktree(revision)
198 self._build_libraries(git_worktree_path)
199 self._gen_code_size_csv(revision, git_worktree_path)
200 self._remove_worktree(git_worktree_path)
201
202 def compare_code_size(self):
203 """Generate results of the size changes between two revisions,
204 old and new. Measured code size results of these two revisions
Xiaofei Bai2400b502021-10-21 12:22:58 +0000205 must be available."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000206
Yanray Wang369cd962023-05-24 17:13:29 +0800207 old_file = open(os.path.join(self.csv_dir, self.old_rev +
208 self.fname_suffix + ".csv"), "r")
209 new_file = open(os.path.join(self.csv_dir, self.new_rev +
210 self.fname_suffix + ".csv"), "r")
211 res_file = open(os.path.join(self.result_dir, "compare-" +
212 self.old_rev + "-" + self.new_rev +
213 self.fname_suffix +
214 ".csv"), "w")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000215
Xiaofei Baibca03e52021-09-09 09:42:37 +0000216 res_file.write("file_name, this_size, old_size, change, change %\n")
Shaun Case8b0ecbc2021-12-20 21:14:10 -0800217 print("Generating comparison results.")
Xiaofei Baibca03e52021-09-09 09:42:37 +0000218
219 old_ds = {}
Yanray Wanga3841ab2023-05-24 18:33:08 +0800220 for line in old_file.readlines():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000221 cols = line.split(", ")
222 fname = cols[0]
223 size = int(cols[1])
224 if size != 0:
225 old_ds[fname] = size
226
227 new_ds = {}
Yanray Wanga3841ab2023-05-24 18:33:08 +0800228 for line in new_file.readlines():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000229 cols = line.split(", ")
230 fname = cols[0]
231 size = int(cols[1])
232 new_ds[fname] = size
233
234 for fname in new_ds:
235 this_size = new_ds[fname]
236 if fname in old_ds:
237 old_size = old_ds[fname]
238 change = this_size - old_size
239 change_pct = change / old_size
240 res_file.write("{}, {}, {}, {}, {:.2%}\n".format(fname, \
241 this_size, old_size, change, float(change_pct)))
242 else:
243 res_file.write("{}, {}\n".format(fname, this_size))
Xiaofei Bai2400b502021-10-21 12:22:58 +0000244 return 0
Xiaofei Baibca03e52021-09-09 09:42:37 +0000245
246 def get_comparision_results(self):
247 """Compare size of library/*.o between self.old_rev and self.new_rev,
248 and generate the result file."""
Gilles Peskined9071e72022-09-18 21:17:09 +0200249 build_tree.check_repo_path()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000250 self._get_code_size_for_rev(self.old_rev)
251 self._get_code_size_for_rev(self.new_rev)
252 return self.compare_code_size()
253
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100254 def _handle_called_process_error(self, e: subprocess.CalledProcessError,
255 git_worktree_path):
256 """Handle a CalledProcessError and quit the program gracefully.
257 Remove any extra worktrees so that the script may be called again."""
258
259 # Tell the user what went wrong
260 print("The following command: {} failed and exited with code {}"
261 .format(e.cmd, e.returncode))
262 print("Process output:\n {}".format(str(e.output, "utf-8")))
263
264 # Quit gracefully by removing the existing worktree
265 self._remove_worktree(git_worktree_path)
266 sys.exit(-1)
267
Xiaofei Bai2400b502021-10-21 12:22:58 +0000268def main():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000269 parser = argparse.ArgumentParser(
270 description=(
271 """This script is for comparing the size of the library files
272 from two different Git revisions within an Mbed TLS repository.
273 The results of the comparison is formatted as csv, and stored at
274 a configurable location.
275 Note: must be run from Mbed TLS root."""
276 )
277 )
278 parser.add_argument(
279 "-r", "--result-dir", type=str, default="comparison",
280 help="directory where comparison result is stored, \
281 default is comparison",
282 )
283 parser.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000284 "-o", "--old-rev", type=str, help="old revision for comparison.",
Xiaofei Baibca03e52021-09-09 09:42:37 +0000285 required=True,
286 )
287 parser.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000288 "-n", "--new-rev", type=str, default=None,
289 help="new revision for comparison, default is the current work \
Shaun Case8b0ecbc2021-12-20 21:14:10 -0800290 directory, including uncommitted changes."
Xiaofei Baibca03e52021-09-09 09:42:37 +0000291 )
Yanray Wang23bd5322023-05-24 11:03:59 +0800292 parser.add_argument(
293 "-a", "--arch", type=str, default=detect_arch(),
294 choices=list(map(lambda s: s.value, SupportedArch)),
295 help="specify architecture for code size comparison, default is the\
296 host architecture."
297 )
Yanray Wang6a862582023-05-24 12:24:38 +0800298 parser.add_argument(
299 "-c", "--config", type=str, default=SupportedConfig.DEFAULT.value,
300 choices=list(map(lambda s: s.value, SupportedConfig)),
301 help="specify configuration type for code size comparison,\
302 default is the current MbedTLS configuration."
303 )
Xiaofei Baibca03e52021-09-09 09:42:37 +0000304 comp_args = parser.parse_args()
305
306 if os.path.isfile(comp_args.result_dir):
307 print("Error: {} is not a directory".format(comp_args.result_dir))
308 parser.exit()
309
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000310 validate_res = CodeSizeComparison.validate_revision(comp_args.old_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000311 old_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai2400b502021-10-21 12:22:58 +0000312
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000313 if comp_args.new_rev is not None:
314 validate_res = CodeSizeComparison.validate_revision(comp_args.new_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000315 new_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000316 else:
317 new_revision = "current"
Xiaofei Bai2400b502021-10-21 12:22:58 +0000318
Yanray Wang6a862582023-05-24 12:24:38 +0800319 print("Measure code size for architecture: {}, configuration: {}"
320 .format(comp_args.arch, comp_args.config))
321 code_size_info = CodeSizeInfo(comp_args.arch, comp_args.config)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000322 result_dir = comp_args.result_dir
Yanray Wang6a862582023-05-24 12:24:38 +0800323 size_compare = CodeSizeComparison(old_revision, new_revision, result_dir,
324 code_size_info)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000325 return_code = size_compare.get_comparision_results()
326 sys.exit(return_code)
327
328
329if __name__ == "__main__":
Xiaofei Bai2400b502021-10-21 12:22:58 +0000330 main()