blob: 5b93b1a30fabe6d67b8b8984e58b84f41903979d [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 )
144 return git_worktree_path
145
146 def _build_libraries(self, git_worktree_path):
147 """Build libraries in the specified worktree."""
148
149 my_environment = os.environ.copy()
150 subprocess.check_output(
Yanray Wang6a862582023-05-24 12:24:38 +0800151 self.make_command, env=my_environment, shell=True,
Xiaofei Baibca03e52021-09-09 09:42:37 +0000152 cwd=git_worktree_path, stderr=subprocess.STDOUT,
153 )
154
155 def _gen_code_size_csv(self, revision, git_worktree_path):
156 """Generate code size csv file."""
157
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000158 csv_fname = revision + ".csv"
159 if revision == "current":
160 print("Measuring code size in current work directory.")
161 else:
162 print("Measuring code size for", revision)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000163 result = subprocess.check_output(
164 ["size library/*.o"], cwd=git_worktree_path, shell=True
165 )
166 size_text = result.decode()
167 csv_file = open(os.path.join(self.csv_dir, csv_fname), "w")
168 for line in size_text.splitlines()[1:]:
169 data = line.split()
170 csv_file.write("{}, {}\n".format(data[5], data[3]))
171
172 def _remove_worktree(self, git_worktree_path):
173 """Remove temporary worktree."""
174 if git_worktree_path != self.repo_path:
175 print("Removing temporary worktree", git_worktree_path)
176 subprocess.check_output(
177 [self.git_command, "worktree", "remove", "--force",
178 git_worktree_path], cwd=self.repo_path,
179 stderr=subprocess.STDOUT
180 )
181
182 def _get_code_size_for_rev(self, revision):
183 """Generate code size csv file for the specified git revision."""
184
185 # Check if the corresponding record exists
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000186 csv_fname = revision + ".csv"
187 if (revision != "current") and \
Xiaofei Baibca03e52021-09-09 09:42:37 +0000188 os.path.exists(os.path.join(self.csv_dir, csv_fname)):
189 print("Code size csv file for", revision, "already exists.")
190 else:
191 git_worktree_path = self._create_git_worktree(revision)
192 self._build_libraries(git_worktree_path)
193 self._gen_code_size_csv(revision, git_worktree_path)
194 self._remove_worktree(git_worktree_path)
195
196 def compare_code_size(self):
197 """Generate results of the size changes between two revisions,
198 old and new. Measured code size results of these two revisions
Xiaofei Bai2400b502021-10-21 12:22:58 +0000199 must be available."""
Xiaofei Baibca03e52021-09-09 09:42:37 +0000200
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000201 old_file = open(os.path.join(self.csv_dir, self.old_rev + ".csv"), "r")
202 new_file = open(os.path.join(self.csv_dir, self.new_rev + ".csv"), "r")
203 res_file = open(os.path.join(self.result_dir, "compare-" + self.old_rev
204 + "-" + self.new_rev + ".csv"), "w")
205
Xiaofei Baibca03e52021-09-09 09:42:37 +0000206 res_file.write("file_name, this_size, old_size, change, change %\n")
Shaun Case8b0ecbc2021-12-20 21:14:10 -0800207 print("Generating comparison results.")
Xiaofei Baibca03e52021-09-09 09:42:37 +0000208
209 old_ds = {}
210 for line in old_file.readlines()[1:]:
211 cols = line.split(", ")
212 fname = cols[0]
213 size = int(cols[1])
214 if size != 0:
215 old_ds[fname] = size
216
217 new_ds = {}
218 for line in new_file.readlines()[1:]:
219 cols = line.split(", ")
220 fname = cols[0]
221 size = int(cols[1])
222 new_ds[fname] = size
223
224 for fname in new_ds:
225 this_size = new_ds[fname]
226 if fname in old_ds:
227 old_size = old_ds[fname]
228 change = this_size - old_size
229 change_pct = change / old_size
230 res_file.write("{}, {}, {}, {}, {:.2%}\n".format(fname, \
231 this_size, old_size, change, float(change_pct)))
232 else:
233 res_file.write("{}, {}\n".format(fname, this_size))
Xiaofei Bai2400b502021-10-21 12:22:58 +0000234 return 0
Xiaofei Baibca03e52021-09-09 09:42:37 +0000235
236 def get_comparision_results(self):
237 """Compare size of library/*.o between self.old_rev and self.new_rev,
238 and generate the result file."""
Gilles Peskined9071e72022-09-18 21:17:09 +0200239 build_tree.check_repo_path()
Xiaofei Baibca03e52021-09-09 09:42:37 +0000240 self._get_code_size_for_rev(self.old_rev)
241 self._get_code_size_for_rev(self.new_rev)
242 return self.compare_code_size()
243
Xiaofei Bai2400b502021-10-21 12:22:58 +0000244def main():
Xiaofei Baibca03e52021-09-09 09:42:37 +0000245 parser = argparse.ArgumentParser(
246 description=(
247 """This script is for comparing the size of the library files
248 from two different Git revisions within an Mbed TLS repository.
249 The results of the comparison is formatted as csv, and stored at
250 a configurable location.
251 Note: must be run from Mbed TLS root."""
252 )
253 )
254 parser.add_argument(
255 "-r", "--result-dir", type=str, default="comparison",
256 help="directory where comparison result is stored, \
257 default is comparison",
258 )
259 parser.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000260 "-o", "--old-rev", type=str, help="old revision for comparison.",
Xiaofei Baibca03e52021-09-09 09:42:37 +0000261 required=True,
262 )
263 parser.add_argument(
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000264 "-n", "--new-rev", type=str, default=None,
265 help="new revision for comparison, default is the current work \
Shaun Case8b0ecbc2021-12-20 21:14:10 -0800266 directory, including uncommitted changes."
Xiaofei Baibca03e52021-09-09 09:42:37 +0000267 )
Yanray Wang23bd5322023-05-24 11:03:59 +0800268 parser.add_argument(
269 "-a", "--arch", type=str, default=detect_arch(),
270 choices=list(map(lambda s: s.value, SupportedArch)),
271 help="specify architecture for code size comparison, default is the\
272 host architecture."
273 )
Yanray Wang6a862582023-05-24 12:24:38 +0800274 parser.add_argument(
275 "-c", "--config", type=str, default=SupportedConfig.DEFAULT.value,
276 choices=list(map(lambda s: s.value, SupportedConfig)),
277 help="specify configuration type for code size comparison,\
278 default is the current MbedTLS configuration."
279 )
Xiaofei Baibca03e52021-09-09 09:42:37 +0000280 comp_args = parser.parse_args()
281
282 if os.path.isfile(comp_args.result_dir):
283 print("Error: {} is not a directory".format(comp_args.result_dir))
284 parser.exit()
285
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000286 validate_res = CodeSizeComparison.validate_revision(comp_args.old_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000287 old_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai2400b502021-10-21 12:22:58 +0000288
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000289 if comp_args.new_rev is not None:
290 validate_res = CodeSizeComparison.validate_revision(comp_args.new_rev)
Xiaofei Baiccd738b2021-11-03 07:12:31 +0000291 new_revision = validate_res.decode().replace("\n", "")
Xiaofei Bai184e8b62021-10-26 09:23:42 +0000292 else:
293 new_revision = "current"
Xiaofei Bai2400b502021-10-21 12:22:58 +0000294
Yanray Wang6a862582023-05-24 12:24:38 +0800295 print("Measure code size for architecture: {}, configuration: {}"
296 .format(comp_args.arch, comp_args.config))
297 code_size_info = CodeSizeInfo(comp_args.arch, comp_args.config)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000298 result_dir = comp_args.result_dir
Yanray Wang6a862582023-05-24 12:24:38 +0800299 size_compare = CodeSizeComparison(old_revision, new_revision, result_dir,
300 code_size_info)
Xiaofei Baibca03e52021-09-09 09:42:37 +0000301 return_code = size_compare.get_comparision_results()
302 sys.exit(return_code)
303
304
305if __name__ == "__main__":
Xiaofei Bai2400b502021-10-21 12:22:58 +0000306 main()