blob: 0145349ae3748be2d9178e78f22d787472be422b [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 \
91 'make -j lib CC=/usr/local/ArmCompilerforEmbedded6.19/bin/armclang \
92 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
Xiaofei Baibca03e52021-09-09 09:42:37 +0000122
123 @staticmethod
Xiaofei Bai2400b502021-10-21 12:22:58 +0000124 def validate_revision(revision):
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000125 result = subprocess.check_output(["git", "rev-parse", "--verify",
126 revision + "^{commit}"], shell=False)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000127 return result
Xiaofei Bai2400b502021-10-21 12:22:58 +0000128
Xiaofei Baibca03e52021-09-09 09:42:37 +0000129 def _create_git_worktree(self, revision):
130 """Make a separate worktree for revision.
131 Do not modify the current worktree."""
132
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000133 if revision == "current":
Xiaofei Baibca03e52021-09-09 09:42:37 +0000134 print("Using current work directory.")
135 git_worktree_path = self.repo_path
136 else:
137 print("Creating git worktree for", revision)
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000138 git_worktree_path = os.path.join(self.repo_path, "temp-" + revision)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000139 subprocess.check_output(
140 [self.git_command, "worktree", "add", "--detach",
141 git_worktree_path, revision], cwd=self.repo_path,
142 stderr=subprocess.STDOUT
143 )
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100144
Xiaofei Baibca03e52021-09-09 09:42:37 +0000145 return git_worktree_path
146
147 def _build_libraries(self, git_worktree_path):
148 """Build libraries in the specified worktree."""
149
150 my_environment = os.environ.copy()
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100151 try:
152 subprocess.check_output(
153 self.make_command, env=my_environment, shell=True,
154 cwd=git_worktree_path, stderr=subprocess.STDOUT,
155 )
156 except subprocess.CalledProcessError as e:
157 self._handle_called_process_error(e, git_worktree_path)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000158
159 def _gen_code_size_csv(self, revision, git_worktree_path):
160 """Generate code size csv file."""
161
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000162 csv_fname = revision + ".csv"
163 if revision == "current":
164 print("Measuring code size in current work directory.")
165 else:
166 print("Measuring code size for", revision)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000167 result = subprocess.check_output(
168 ["size library/*.o"], cwd=git_worktree_path, shell=True
169 )
170 size_text = result.decode()
171 csv_file = open(os.path.join(self.csv_dir, csv_fname), "w")
172 for line in size_text.splitlines()[1:]:
173 data = line.split()
174 csv_file.write("{}, {}\n".format(data[5], data[3]))
175
176 def _remove_worktree(self, git_worktree_path):
177 """Remove temporary worktree."""
178 if git_worktree_path != self.repo_path:
179 print("Removing temporary worktree", git_worktree_path)
180 subprocess.check_output(
181 [self.git_command, "worktree", "remove", "--force",
182 git_worktree_path], cwd=self.repo_path,
183 stderr=subprocess.STDOUT
184 )
185
186 def _get_code_size_for_rev(self, revision):
187 """Generate code size csv file for the specified git revision."""
188
189 # Check if the corresponding record exists
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000190 csv_fname = revision + ".csv"
191 if (revision != "current") and \
Xiaofei Baibca03e52021-09-09 09:42:37 +0000192 os.path.exists(os.path.join(self.csv_dir, csv_fname)):
193 print("Code size csv file for", revision, "already exists.")
194 else:
195 git_worktree_path = self._create_git_worktree(revision)
196 self._build_libraries(git_worktree_path)
197 self._gen_code_size_csv(revision, git_worktree_path)
198 self._remove_worktree(git_worktree_path)
199
200 def compare_code_size(self):
201 """Generate results of the size changes between two revisions,
202 old and new. Measured code size results of these two revisions
Xiaofei Bai2400b502021-10-21 12:22:58 +0000203 must be available."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000204
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000205 old_file = open(os.path.join(self.csv_dir, self.old_rev + ".csv"), "r")
206 new_file = open(os.path.join(self.csv_dir, self.new_rev + ".csv"), "r")
207 res_file = open(os.path.join(self.result_dir, "compare-" + self.old_rev
208 + "-" + self.new_rev + ".csv"), "w")
209
Xiaofei Baibca03e52021-09-09 09:42:37 +0000210 res_file.write("file_name, this_size, old_size, change, change %\n")
Shaun Case8b0ecbc2021-12-20 21:14:10 -0800211 print("Generating comparison results.")
Xiaofei Baibca03e52021-09-09 09:42:37 +0000212
213 old_ds = {}
214 for line in old_file.readlines()[1:]:
215 cols = line.split(", ")
216 fname = cols[0]
217 size = int(cols[1])
218 if size != 0:
219 old_ds[fname] = size
220
221 new_ds = {}
222 for line in new_file.readlines()[1:]:
223 cols = line.split(", ")
224 fname = cols[0]
225 size = int(cols[1])
226 new_ds[fname] = size
227
228 for fname in new_ds:
229 this_size = new_ds[fname]
230 if fname in old_ds:
231 old_size = old_ds[fname]
232 change = this_size - old_size
233 change_pct = change / old_size
234 res_file.write("{}, {}, {}, {}, {:.2%}\n".format(fname, \
235 this_size, old_size, change, float(change_pct)))
236 else:
237 res_file.write("{}, {}\n".format(fname, this_size))
Xiaofei Bai2400b502021-10-21 12:22:58 +0000238 return 0
Xiaofei Baibca03e52021-09-09 09:42:37 +0000239
240 def get_comparision_results(self):
241 """Compare size of library/*.o between self.old_rev and self.new_rev,
242 and generate the result file."""
Gilles Peskined9071e72022-09-18 21:17:09 +0200243 build_tree.check_repo_path()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000244 self._get_code_size_for_rev(self.old_rev)
245 self._get_code_size_for_rev(self.new_rev)
246 return self.compare_code_size()
247
Aditya Deshpande41a0aad2023-04-13 16:32:21 +0100248 def _handle_called_process_error(self, e: subprocess.CalledProcessError,
249 git_worktree_path):
250 """Handle a CalledProcessError and quit the program gracefully.
251 Remove any extra worktrees so that the script may be called again."""
252
253 # Tell the user what went wrong
254 print("The following command: {} failed and exited with code {}"
255 .format(e.cmd, e.returncode))
256 print("Process output:\n {}".format(str(e.output, "utf-8")))
257
258 # Quit gracefully by removing the existing worktree
259 self._remove_worktree(git_worktree_path)
260 sys.exit(-1)
261
Xiaofei Bai2400b502021-10-21 12:22:58 +0000262def main():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000263 parser = argparse.ArgumentParser(
264 description=(
265 """This script is for comparing the size of the library files
266 from two different Git revisions within an Mbed TLS repository.
267 The results of the comparison is formatted as csv, and stored at
268 a configurable location.
269 Note: must be run from Mbed TLS root."""
270 )
271 )
272 parser.add_argument(
273 "-r", "--result-dir", type=str, default="comparison",
274 help="directory where comparison result is stored, \
275 default is comparison",
276 )
277 parser.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000278 "-o", "--old-rev", type=str, help="old revision for comparison.",
Xiaofei Baibca03e52021-09-09 09:42:37 +0000279 required=True,
280 )
281 parser.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000282 "-n", "--new-rev", type=str, default=None,
283 help="new revision for comparison, default is the current work \
Shaun Case8b0ecbc2021-12-20 21:14:10 -0800284 directory, including uncommitted changes."
Xiaofei Baibca03e52021-09-09 09:42:37 +0000285 )
Yanray Wang23bd5322023-05-24 11:03:59 +0800286 parser.add_argument(
287 "-a", "--arch", type=str, default=detect_arch(),
288 choices=list(map(lambda s: s.value, SupportedArch)),
289 help="specify architecture for code size comparison, default is the\
290 host architecture."
291 )
Yanray Wang6a862582023-05-24 12:24:38 +0800292 parser.add_argument(
293 "-c", "--config", type=str, default=SupportedConfig.DEFAULT.value,
294 choices=list(map(lambda s: s.value, SupportedConfig)),
295 help="specify configuration type for code size comparison,\
296 default is the current MbedTLS configuration."
297 )
Xiaofei Baibca03e52021-09-09 09:42:37 +0000298 comp_args = parser.parse_args()
299
300 if os.path.isfile(comp_args.result_dir):
301 print("Error: {} is not a directory".format(comp_args.result_dir))
302 parser.exit()
303
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000304 validate_res = CodeSizeComparison.validate_revision(comp_args.old_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000305 old_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai2400b502021-10-21 12:22:58 +0000306
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000307 if comp_args.new_rev is not None:
308 validate_res = CodeSizeComparison.validate_revision(comp_args.new_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000309 new_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000310 else:
311 new_revision = "current"
Xiaofei Bai2400b502021-10-21 12:22:58 +0000312
Yanray Wang6a862582023-05-24 12:24:38 +0800313 print("Measure code size for architecture: {}, configuration: {}"
314 .format(comp_args.arch, comp_args.config))
315 code_size_info = CodeSizeInfo(comp_args.arch, comp_args.config)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000316 result_dir = comp_args.result_dir
Yanray Wang6a862582023-05-24 12:24:38 +0800317 size_compare = CodeSizeComparison(old_revision, new_revision, result_dir,
318 code_size_info)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000319 return_code = size_compare.get_comparision_results()
320 sys.exit(return_code)
321
322
323if __name__ == "__main__":
Xiaofei Bai2400b502021-10-21 12:22:58 +0000324 main()