blob: abd13df240fc994e06108b03323436bc922dc277 [file] [log] [blame]
Tom Rini0344c602024-10-08 13:56:50 -06001#!/usr/bin/env python3
2
3"""
4This script is for comparing the size of the library files from two
5different Git revisions within an Mbed TLS repository.
6The results of the comparison is formatted as csv and stored at a
7configurable location.
8Note: must be run from Mbed TLS root.
9"""
10
11# Copyright The Mbed TLS Contributors
12# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
13
14import argparse
15import logging
16import os
17import re
18import shutil
19import subprocess
20import sys
21import typing
22from enum import Enum
23
24from mbedtls_dev import build_tree
25from mbedtls_dev import logging_util
26from mbedtls_dev import typing_util
27
28class SupportedArch(Enum):
29 """Supported architecture for code size measurement."""
30 AARCH64 = 'aarch64'
31 AARCH32 = 'aarch32'
32 ARMV8_M = 'armv8-m'
33 X86_64 = 'x86_64'
34 X86 = 'x86'
35
36
37class SupportedConfig(Enum):
38 """Supported configuration for code size measurement."""
39 DEFAULT = 'default'
40 TFM_MEDIUM = 'tfm-medium'
41
42
43# Static library
44MBEDTLS_STATIC_LIB = {
45 'CRYPTO': 'library/libmbedcrypto.a',
46 'X509': 'library/libmbedx509.a',
47 'TLS': 'library/libmbedtls.a',
48}
49
50class CodeSizeDistinctInfo: # pylint: disable=too-few-public-methods
51 """Data structure to store possibly distinct information for code size
52 comparison."""
53 def __init__( #pylint: disable=too-many-arguments
54 self,
55 version: str,
56 git_rev: str,
57 arch: str,
58 config: str,
59 compiler: str,
60 opt_level: str,
61 ) -> None:
62 """
63 :param: version: which version to compare with for code size.
64 :param: git_rev: Git revision to calculate code size.
65 :param: arch: architecture to measure code size on.
66 :param: config: Configuration type to calculate code size.
67 (See SupportedConfig)
68 :param: compiler: compiler used to build library/*.o.
69 :param: opt_level: Options that control optimization. (E.g. -Os)
70 """
71 self.version = version
72 self.git_rev = git_rev
73 self.arch = arch
74 self.config = config
75 self.compiler = compiler
76 self.opt_level = opt_level
77 # Note: Variables below are not initialized by class instantiation.
78 self.pre_make_cmd = [] #type: typing.List[str]
79 self.make_cmd = ''
80
81 def get_info_indication(self):
82 """Return a unique string to indicate Code Size Distinct Information."""
83 return '{git_rev}-{arch}-{config}-{compiler}'.format(**self.__dict__)
84
85
86class CodeSizeCommonInfo: # pylint: disable=too-few-public-methods
87 """Data structure to store common information for code size comparison."""
88 def __init__(
89 self,
90 host_arch: str,
91 measure_cmd: str,
92 ) -> None:
93 """
94 :param host_arch: host architecture.
95 :param measure_cmd: command to measure code size for library/*.o.
96 """
97 self.host_arch = host_arch
98 self.measure_cmd = measure_cmd
99
100 def get_info_indication(self):
101 """Return a unique string to indicate Code Size Common Information."""
102 return '{measure_tool}'\
103 .format(measure_tool=self.measure_cmd.strip().split(' ')[0])
104
105class CodeSizeResultInfo: # pylint: disable=too-few-public-methods
106 """Data structure to store result options for code size comparison."""
107 def __init__( #pylint: disable=too-many-arguments
108 self,
109 record_dir: str,
110 comp_dir: str,
111 with_markdown=False,
112 stdout=False,
113 show_all=False,
114 ) -> None:
115 """
116 :param record_dir: directory to store code size record.
117 :param comp_dir: directory to store results of code size comparision.
118 :param with_markdown: write comparision result into a markdown table.
119 (Default: False)
120 :param stdout: direct comparison result into sys.stdout.
121 (Default False)
122 :param show_all: show all objects in comparison result. (Default False)
123 """
124 self.record_dir = record_dir
125 self.comp_dir = comp_dir
126 self.with_markdown = with_markdown
127 self.stdout = stdout
128 self.show_all = show_all
129
130
131DETECT_ARCH_CMD = "cc -dM -E - < /dev/null"
132def detect_arch() -> str:
133 """Auto-detect host architecture."""
134 cc_output = subprocess.check_output(DETECT_ARCH_CMD, shell=True).decode()
135 if '__aarch64__' in cc_output:
136 return SupportedArch.AARCH64.value
137 if '__arm__' in cc_output:
138 return SupportedArch.AARCH32.value
139 if '__x86_64__' in cc_output:
140 return SupportedArch.X86_64.value
141 if '__i386__' in cc_output:
142 return SupportedArch.X86.value
143 else:
144 print("Unknown host architecture, cannot auto-detect arch.")
145 sys.exit(1)
146
147TFM_MEDIUM_CONFIG_H = 'configs/ext/tfm_mbedcrypto_config_profile_medium.h'
148TFM_MEDIUM_CRYPTO_CONFIG_H = 'configs/ext/crypto_config_profile_medium.h'
149
150CONFIG_H = 'include/mbedtls/mbedtls_config.h'
151CRYPTO_CONFIG_H = 'include/psa/crypto_config.h'
152BACKUP_SUFFIX = '.code_size.bak'
153
154class CodeSizeBuildInfo: # pylint: disable=too-few-public-methods
155 """Gather information used to measure code size.
156
157 It collects information about architecture, configuration in order to
158 infer build command for code size measurement.
159 """
160
161 SupportedArchConfig = [
162 '-a ' + SupportedArch.AARCH64.value + ' -c ' + SupportedConfig.DEFAULT.value,
163 '-a ' + SupportedArch.AARCH32.value + ' -c ' + SupportedConfig.DEFAULT.value,
164 '-a ' + SupportedArch.X86_64.value + ' -c ' + SupportedConfig.DEFAULT.value,
165 '-a ' + SupportedArch.X86.value + ' -c ' + SupportedConfig.DEFAULT.value,
166 '-a ' + SupportedArch.ARMV8_M.value + ' -c ' + SupportedConfig.TFM_MEDIUM.value,
167 ]
168
169 def __init__(
170 self,
171 size_dist_info: CodeSizeDistinctInfo,
172 host_arch: str,
173 logger: logging.Logger,
174 ) -> None:
175 """
176 :param size_dist_info:
177 CodeSizeDistinctInfo containing info for code size measurement.
178 - size_dist_info.arch: architecture to measure code size on.
179 - size_dist_info.config: configuration type to measure
180 code size with.
181 - size_dist_info.compiler: compiler used to build library/*.o.
182 - size_dist_info.opt_level: Options that control optimization.
183 (E.g. -Os)
184 :param host_arch: host architecture.
185 :param logger: logging module
186 """
187 self.arch = size_dist_info.arch
188 self.config = size_dist_info.config
189 self.compiler = size_dist_info.compiler
190 self.opt_level = size_dist_info.opt_level
191
192 self.make_cmd = ['make', '-j', 'lib']
193
194 self.host_arch = host_arch
195 self.logger = logger
196
197 def check_correctness(self) -> bool:
198 """Check whether we are using proper / supported combination
199 of information to build library/*.o."""
200
201 # default config
202 if self.config == SupportedConfig.DEFAULT.value and \
203 self.arch == self.host_arch:
204 return True
205 # TF-M
206 elif self.arch == SupportedArch.ARMV8_M.value and \
207 self.config == SupportedConfig.TFM_MEDIUM.value:
208 return True
209
210 return False
211
212 def infer_pre_make_command(self) -> typing.List[str]:
213 """Infer command to set up proper configuration before running make."""
214 pre_make_cmd = [] #type: typing.List[str]
215 if self.config == SupportedConfig.TFM_MEDIUM.value:
216 pre_make_cmd.append('cp {src} {dest}'
217 .format(src=TFM_MEDIUM_CONFIG_H, dest=CONFIG_H))
218 pre_make_cmd.append('cp {src} {dest}'
219 .format(src=TFM_MEDIUM_CRYPTO_CONFIG_H,
220 dest=CRYPTO_CONFIG_H))
221
222 return pre_make_cmd
223
224 def infer_make_cflags(self) -> str:
225 """Infer CFLAGS by instance attributes in CodeSizeDistinctInfo."""
226 cflags = [] #type: typing.List[str]
227
228 # set optimization level
229 cflags.append(self.opt_level)
230 # set compiler by config
231 if self.config == SupportedConfig.TFM_MEDIUM.value:
232 self.compiler = 'armclang'
233 cflags.append('-mcpu=cortex-m33')
234 # set target
235 if self.compiler == 'armclang':
236 cflags.append('--target=arm-arm-none-eabi')
237
238 return ' '.join(cflags)
239
240 def infer_make_command(self) -> str:
241 """Infer make command by CFLAGS and CC."""
242
243 if self.check_correctness():
244 # set CFLAGS=
245 self.make_cmd.append('CFLAGS=\'{}\''.format(self.infer_make_cflags()))
246 # set CC=
247 self.make_cmd.append('CC={}'.format(self.compiler))
248 return ' '.join(self.make_cmd)
249 else:
250 self.logger.error("Unsupported combination of architecture: {} " \
251 "and configuration: {}.\n"
252 .format(self.arch,
253 self.config))
254 self.logger.error("Please use supported combination of " \
255 "architecture and configuration:")
256 for comb in CodeSizeBuildInfo.SupportedArchConfig:
257 self.logger.error(comb)
258 self.logger.error("")
259 self.logger.error("For your system, please use:")
260 for comb in CodeSizeBuildInfo.SupportedArchConfig:
261 if "default" in comb and self.host_arch not in comb:
262 continue
263 self.logger.error(comb)
264 sys.exit(1)
265
266
267class CodeSizeCalculator:
268 """ A calculator to calculate code size of library/*.o based on
269 Git revision and code size measurement tool.
270 """
271
272 def __init__( #pylint: disable=too-many-arguments
273 self,
274 git_rev: str,
275 pre_make_cmd: typing.List[str],
276 make_cmd: str,
277 measure_cmd: str,
278 logger: logging.Logger,
279 ) -> None:
280 """
281 :param git_rev: Git revision. (E.g: commit)
282 :param pre_make_cmd: command to set up proper config before running make.
283 :param make_cmd: command to build library/*.o.
284 :param measure_cmd: command to measure code size for library/*.o.
285 :param logger: logging module
286 """
287 self.repo_path = "."
288 self.git_command = "git"
289 self.make_clean = 'make clean'
290
291 self.git_rev = git_rev
292 self.pre_make_cmd = pre_make_cmd
293 self.make_cmd = make_cmd
294 self.measure_cmd = measure_cmd
295 self.logger = logger
296
297 @staticmethod
298 def validate_git_revision(git_rev: str) -> str:
299 result = subprocess.check_output(["git", "rev-parse", "--verify",
300 git_rev + "^{commit}"],
301 shell=False, universal_newlines=True)
302 return result[:7]
303
304 def _create_git_worktree(self) -> str:
305 """Create a separate worktree for Git revision.
306 If Git revision is current, use current worktree instead."""
307
308 if self.git_rev == 'current':
309 self.logger.debug("Using current work directory.")
310 git_worktree_path = self.repo_path
311 else:
312 self.logger.debug("Creating git worktree for {}."
313 .format(self.git_rev))
314 git_worktree_path = os.path.join(self.repo_path,
315 "temp-" + self.git_rev)
316 subprocess.check_output(
317 [self.git_command, "worktree", "add", "--detach",
318 git_worktree_path, self.git_rev], cwd=self.repo_path,
319 stderr=subprocess.STDOUT
320 )
321
322 return git_worktree_path
323
324 @staticmethod
325 def backup_config_files(restore: bool) -> None:
326 """Backup / Restore config files."""
327 if restore:
328 shutil.move(CONFIG_H + BACKUP_SUFFIX, CONFIG_H)
329 shutil.move(CRYPTO_CONFIG_H + BACKUP_SUFFIX, CRYPTO_CONFIG_H)
330 else:
331 shutil.copy(CONFIG_H, CONFIG_H + BACKUP_SUFFIX)
332 shutil.copy(CRYPTO_CONFIG_H, CRYPTO_CONFIG_H + BACKUP_SUFFIX)
333
334 def _build_libraries(self, git_worktree_path: str) -> None:
335 """Build library/*.o in the specified worktree."""
336
337 self.logger.debug("Building library/*.o for {}."
338 .format(self.git_rev))
339 my_environment = os.environ.copy()
340 try:
341 if self.git_rev == 'current':
342 self.backup_config_files(restore=False)
343 for pre_cmd in self.pre_make_cmd:
344 subprocess.check_output(
345 pre_cmd, env=my_environment, shell=True,
346 cwd=git_worktree_path, stderr=subprocess.STDOUT,
347 universal_newlines=True
348 )
349 subprocess.check_output(
350 self.make_clean, env=my_environment, shell=True,
351 cwd=git_worktree_path, stderr=subprocess.STDOUT,
352 universal_newlines=True
353 )
354 subprocess.check_output(
355 self.make_cmd, env=my_environment, shell=True,
356 cwd=git_worktree_path, stderr=subprocess.STDOUT,
357 universal_newlines=True
358 )
359 if self.git_rev == 'current':
360 self.backup_config_files(restore=True)
361 except subprocess.CalledProcessError as e:
362 self._handle_called_process_error(e, git_worktree_path)
363
364 def _gen_raw_code_size(self, git_worktree_path: str) -> typing.Dict[str, str]:
365 """Measure code size by a tool and return in UTF-8 encoding."""
366
367 self.logger.debug("Measuring code size for {} by `{}`."
368 .format(self.git_rev,
369 self.measure_cmd.strip().split(' ')[0]))
370
371 res = {}
372 for mod, st_lib in MBEDTLS_STATIC_LIB.items():
373 try:
374 result = subprocess.check_output(
375 [self.measure_cmd + ' ' + st_lib], cwd=git_worktree_path,
376 shell=True, universal_newlines=True
377 )
378 res[mod] = result
379 except subprocess.CalledProcessError as e:
380 self._handle_called_process_error(e, git_worktree_path)
381
382 return res
383
384 def _remove_worktree(self, git_worktree_path: str) -> None:
385 """Remove temporary worktree."""
386 if git_worktree_path != self.repo_path:
387 self.logger.debug("Removing temporary worktree {}."
388 .format(git_worktree_path))
389 subprocess.check_output(
390 [self.git_command, "worktree", "remove", "--force",
391 git_worktree_path], cwd=self.repo_path,
392 stderr=subprocess.STDOUT
393 )
394
395 def _handle_called_process_error(self, e: subprocess.CalledProcessError,
396 git_worktree_path: str) -> None:
397 """Handle a CalledProcessError and quit the program gracefully.
398 Remove any extra worktrees so that the script may be called again."""
399
400 # Tell the user what went wrong
401 self.logger.error(e, exc_info=True)
402 self.logger.error("Process output:\n {}".format(e.output))
403
404 # Quit gracefully by removing the existing worktree
405 self._remove_worktree(git_worktree_path)
406 sys.exit(-1)
407
408 def cal_libraries_code_size(self) -> typing.Dict[str, str]:
409 """Do a complete round to calculate code size of library/*.o
410 by measurement tool.
411
412 :return A dictionary of measured code size
413 - typing.Dict[mod: str]
414 """
415
416 git_worktree_path = self._create_git_worktree()
417 try:
418 self._build_libraries(git_worktree_path)
419 res = self._gen_raw_code_size(git_worktree_path)
420 finally:
421 self._remove_worktree(git_worktree_path)
422
423 return res
424
425
426class CodeSizeGenerator:
427 """ A generator based on size measurement tool for library/*.o.
428
429 This is an abstract class. To use it, derive a class that implements
430 write_record and write_comparison methods, then call both of them with
431 proper arguments.
432 """
433 def __init__(self, logger: logging.Logger) -> None:
434 """
435 :param logger: logging module
436 """
437 self.logger = logger
438
439 def write_record(
440 self,
441 git_rev: str,
442 code_size_text: typing.Dict[str, str],
443 output: typing_util.Writable
444 ) -> None:
445 """Write size record into a file.
446
447 :param git_rev: Git revision. (E.g: commit)
448 :param code_size_text:
449 string output (utf-8) from measurement tool of code size.
450 - typing.Dict[mod: str]
451 :param output: output stream which the code size record is written to.
452 (Note: Normally write code size record into File)
453 """
454 raise NotImplementedError
455
456 def write_comparison( #pylint: disable=too-many-arguments
457 self,
458 old_rev: str,
459 new_rev: str,
460 output: typing_util.Writable,
461 with_markdown=False,
462 show_all=False
463 ) -> None:
464 """Write a comparision result into a stream between two Git revisions.
465
466 :param old_rev: old Git revision to compared with.
467 :param new_rev: new Git revision to compared with.
468 :param output: output stream which the code size record is written to.
469 (File / sys.stdout)
470 :param with_markdown: write comparision result in a markdown table.
471 (Default: False)
472 :param show_all: show all objects in comparison result. (Default False)
473 """
474 raise NotImplementedError
475
476
477class CodeSizeGeneratorWithSize(CodeSizeGenerator):
478 """Code Size Base Class for size record saving and writing."""
479
480 class SizeEntry: # pylint: disable=too-few-public-methods
481 """Data Structure to only store information of code size."""
482 def __init__(self, text: int, data: int, bss: int, dec: int):
483 self.text = text
484 self.data = data
485 self.bss = bss
486 self.total = dec # total <=> dec
487
488 def __init__(self, logger: logging.Logger) -> None:
489 """ Variable code_size is used to store size info for any Git revisions.
490 :param code_size:
491 Data Format as following:
492 code_size = {
493 git_rev: {
494 module: {
495 file_name: SizeEntry,
496 ...
497 },
498 ...
499 },
500 ...
501 }
502 """
503 super().__init__(logger)
504 self.code_size = {} #type: typing.Dict[str, typing.Dict]
505 self.mod_total_suffix = '-' + 'TOTALS'
506
507 def _set_size_record(self, git_rev: str, mod: str, size_text: str) -> None:
508 """Store size information for target Git revision and high-level module.
509
510 size_text Format: text data bss dec hex filename
511 """
512 size_record = {}
513 for line in size_text.splitlines()[1:]:
514 data = line.split()
515 if re.match(r'\s*\(TOTALS\)', data[5]):
516 data[5] = mod + self.mod_total_suffix
517 # file_name: SizeEntry(text, data, bss, dec)
518 size_record[data[5]] = CodeSizeGeneratorWithSize.SizeEntry(
519 int(data[0]), int(data[1]), int(data[2]), int(data[3]))
520 self.code_size.setdefault(git_rev, {}).update({mod: size_record})
521
522 def read_size_record(self, git_rev: str, fname: str) -> None:
523 """Read size information from csv file and write it into code_size.
524
525 fname Format: filename text data bss dec
526 """
527 mod = ""
528 size_record = {}
529 with open(fname, 'r') as csv_file:
530 for line in csv_file:
531 data = line.strip().split()
532 # check if we find the beginning of a module
533 if data and data[0] in MBEDTLS_STATIC_LIB:
534 mod = data[0]
535 continue
536
537 if mod:
538 # file_name: SizeEntry(text, data, bss, dec)
539 size_record[data[0]] = CodeSizeGeneratorWithSize.SizeEntry(
540 int(data[1]), int(data[2]), int(data[3]), int(data[4]))
541
542 # check if we hit record for the end of a module
543 m = re.match(r'\w+' + self.mod_total_suffix, line)
544 if m:
545 if git_rev in self.code_size:
546 self.code_size[git_rev].update({mod: size_record})
547 else:
548 self.code_size[git_rev] = {mod: size_record}
549 mod = ""
550 size_record = {}
551
552 def write_record(
553 self,
554 git_rev: str,
555 code_size_text: typing.Dict[str, str],
556 output: typing_util.Writable
557 ) -> None:
558 """Write size information to a file.
559
560 Writing Format: filename text data bss total(dec)
561 """
562 for mod, size_text in code_size_text.items():
563 self._set_size_record(git_rev, mod, size_text)
564
565 format_string = "{:<30} {:>7} {:>7} {:>7} {:>7}\n"
566 output.write(format_string.format("filename",
567 "text", "data", "bss", "total"))
568
569 for mod, f_size in self.code_size[git_rev].items():
570 output.write("\n" + mod + "\n")
571 for fname, size_entry in f_size.items():
572 output.write(format_string
573 .format(fname,
574 size_entry.text, size_entry.data,
575 size_entry.bss, size_entry.total))
576
577 def write_comparison( #pylint: disable=too-many-arguments
578 self,
579 old_rev: str,
580 new_rev: str,
581 output: typing_util.Writable,
582 with_markdown=False,
583 show_all=False
584 ) -> None:
585 # pylint: disable=too-many-locals
586 """Write comparison result into a file.
587
588 Writing Format:
589 Markdown Output:
590 filename new(text) new(data) change(text) change(data)
591 CSV Output:
592 filename new(text) new(data) old(text) old(data) change(text) change(data)
593 """
594 header_line = ["filename", "new(text)", "old(text)", "change(text)",
595 "new(data)", "old(data)", "change(data)"]
596 if with_markdown:
597 dash_line = [":----", "----:", "----:", "----:",
598 "----:", "----:", "----:"]
599 # | filename | new(text) | new(data) | change(text) | change(data) |
600 line_format = "| {0:<30} | {1:>9} | {4:>9} | {3:>12} | {6:>12} |\n"
601 bold_text = lambda x: '**' + str(x) + '**'
602 else:
603 # filename new(text) new(data) old(text) old(data) change(text) change(data)
604 line_format = "{0:<30} {1:>9} {4:>9} {2:>10} {5:>10} {3:>12} {6:>12}\n"
605
606 def cal_sect_change(
607 old_size: typing.Optional[CodeSizeGeneratorWithSize.SizeEntry],
608 new_size: typing.Optional[CodeSizeGeneratorWithSize.SizeEntry],
609 sect: str
610 ) -> typing.List:
611 """Inner helper function to calculate size change for a section.
612
613 Convention for special cases:
614 - If the object has been removed in new Git revision,
615 the size is minus code size of old Git revision;
616 the size change is marked as `Removed`,
617 - If the object only exists in new Git revision,
618 the size is code size of new Git revision;
619 the size change is marked as `None`,
620
621 :param: old_size: code size for objects in old Git revision.
622 :param: new_size: code size for objects in new Git revision.
623 :param: sect: section to calculate from `size` tool. This could be
624 any instance variable in SizeEntry.
625 :return: List of [section size of objects for new Git revision,
626 section size of objects for old Git revision,
627 section size change of objects between two Git revisions]
628 """
629 if old_size and new_size:
630 new_attr = new_size.__dict__[sect]
631 old_attr = old_size.__dict__[sect]
632 delta = new_attr - old_attr
633 change_attr = '{0:{1}}'.format(delta, '+' if delta else '')
634 elif old_size:
635 new_attr = 'Removed'
636 old_attr = old_size.__dict__[sect]
637 delta = - old_attr
638 change_attr = '{0:{1}}'.format(delta, '+' if delta else '')
639 elif new_size:
640 new_attr = new_size.__dict__[sect]
641 old_attr = 'NotCreated'
642 delta = new_attr
643 change_attr = '{0:{1}}'.format(delta, '+' if delta else '')
644 else:
645 # Should never happen
646 new_attr = 'Error'
647 old_attr = 'Error'
648 change_attr = 'Error'
649 return [new_attr, old_attr, change_attr]
650
651 # sort dictionary by key
652 sort_by_k = lambda item: item[0].lower()
653 def get_results(
654 f_rev_size:
655 typing.Dict[str,
656 typing.Dict[str,
657 CodeSizeGeneratorWithSize.SizeEntry]]
658 ) -> typing.List:
659 """Return List of results in the format of:
660 [filename, new(text), old(text), change(text),
661 new(data), old(data), change(data)]
662 """
663 res = []
664 for fname, revs_size in sorted(f_rev_size.items(), key=sort_by_k):
665 old_size = revs_size.get(old_rev)
666 new_size = revs_size.get(new_rev)
667
668 text_sect = cal_sect_change(old_size, new_size, 'text')
669 data_sect = cal_sect_change(old_size, new_size, 'data')
670 # skip the files that haven't changed in code size
671 if not show_all and text_sect[-1] == '0' and data_sect[-1] == '0':
672 continue
673
674 res.append([fname, *text_sect, *data_sect])
675 return res
676
677 # write header
678 output.write(line_format.format(*header_line))
679 if with_markdown:
680 output.write(line_format.format(*dash_line))
681 for mod in MBEDTLS_STATIC_LIB:
682 # convert self.code_size to:
683 # {
684 # file_name: {
685 # old_rev: SizeEntry,
686 # new_rev: SizeEntry
687 # },
688 # ...
689 # }
690 f_rev_size = {} #type: typing.Dict[str, typing.Dict]
691 for fname, size_entry in self.code_size[old_rev][mod].items():
692 f_rev_size.setdefault(fname, {}).update({old_rev: size_entry})
693 for fname, size_entry in self.code_size[new_rev][mod].items():
694 f_rev_size.setdefault(fname, {}).update({new_rev: size_entry})
695
696 mod_total_sz = f_rev_size.pop(mod + self.mod_total_suffix)
697 res = get_results(f_rev_size)
698 total_clm = get_results({mod + self.mod_total_suffix: mod_total_sz})
699 if with_markdown:
700 # bold row of mod-TOTALS in markdown table
701 total_clm = [[bold_text(j) for j in i] for i in total_clm]
702 res += total_clm
703
704 # write comparison result
705 for line in res:
706 output.write(line_format.format(*line))
707
708
709class CodeSizeComparison:
710 """Compare code size between two Git revisions."""
711
712 def __init__( #pylint: disable=too-many-arguments
713 self,
714 old_size_dist_info: CodeSizeDistinctInfo,
715 new_size_dist_info: CodeSizeDistinctInfo,
716 size_common_info: CodeSizeCommonInfo,
717 result_options: CodeSizeResultInfo,
718 logger: logging.Logger,
719 ) -> None:
720 """
721 :param old_size_dist_info: CodeSizeDistinctInfo containing old distinct
722 info to compare code size with.
723 :param new_size_dist_info: CodeSizeDistinctInfo containing new distinct
724 info to take as comparision base.
725 :param size_common_info: CodeSizeCommonInfo containing common info for
726 both old and new size distinct info and
727 measurement tool.
728 :param result_options: CodeSizeResultInfo containing results options for
729 code size record and comparision.
730 :param logger: logging module
731 """
732
733 self.logger = logger
734
735 self.old_size_dist_info = old_size_dist_info
736 self.new_size_dist_info = new_size_dist_info
737 self.size_common_info = size_common_info
738 # infer pre make command
739 self.old_size_dist_info.pre_make_cmd = CodeSizeBuildInfo(
740 self.old_size_dist_info, self.size_common_info.host_arch,
741 self.logger).infer_pre_make_command()
742 self.new_size_dist_info.pre_make_cmd = CodeSizeBuildInfo(
743 self.new_size_dist_info, self.size_common_info.host_arch,
744 self.logger).infer_pre_make_command()
745 # infer make command
746 self.old_size_dist_info.make_cmd = CodeSizeBuildInfo(
747 self.old_size_dist_info, self.size_common_info.host_arch,
748 self.logger).infer_make_command()
749 self.new_size_dist_info.make_cmd = CodeSizeBuildInfo(
750 self.new_size_dist_info, self.size_common_info.host_arch,
751 self.logger).infer_make_command()
752 # initialize size parser with corresponding measurement tool
753 self.code_size_generator = self.__generate_size_parser()
754
755 self.result_options = result_options
756 self.csv_dir = os.path.abspath(self.result_options.record_dir)
757 os.makedirs(self.csv_dir, exist_ok=True)
758 self.comp_dir = os.path.abspath(self.result_options.comp_dir)
759 os.makedirs(self.comp_dir, exist_ok=True)
760
761 def __generate_size_parser(self):
762 """Generate a parser for the corresponding measurement tool."""
763 if re.match(r'size', self.size_common_info.measure_cmd.strip()):
764 return CodeSizeGeneratorWithSize(self.logger)
765 else:
766 self.logger.error("Unsupported measurement tool: `{}`."
767 .format(self.size_common_info.measure_cmd
768 .strip().split(' ')[0]))
769 sys.exit(1)
770
771 def cal_code_size(
772 self,
773 size_dist_info: CodeSizeDistinctInfo
774 ) -> typing.Dict[str, str]:
775 """Calculate code size of library/*.o in a UTF-8 encoding"""
776
777 return CodeSizeCalculator(size_dist_info.git_rev,
778 size_dist_info.pre_make_cmd,
779 size_dist_info.make_cmd,
780 self.size_common_info.measure_cmd,
781 self.logger).cal_libraries_code_size()
782
783 def gen_code_size_report(self, size_dist_info: CodeSizeDistinctInfo) -> None:
784 """Generate code size record and write it into a file."""
785
786 self.logger.info("Start to generate code size record for {}."
787 .format(size_dist_info.git_rev))
788 output_file = os.path.join(
789 self.csv_dir,
790 '{}-{}.csv'
791 .format(size_dist_info.get_info_indication(),
792 self.size_common_info.get_info_indication()))
793 # Check if the corresponding record exists
794 if size_dist_info.git_rev != "current" and \
795 os.path.exists(output_file):
796 self.logger.debug("Code size csv file for {} already exists."
797 .format(size_dist_info.git_rev))
798 self.code_size_generator.read_size_record(
799 size_dist_info.git_rev, output_file)
800 else:
801 # measure code size
802 code_size_text = self.cal_code_size(size_dist_info)
803
804 self.logger.debug("Generating code size csv for {}."
805 .format(size_dist_info.git_rev))
806 output = open(output_file, "w")
807 self.code_size_generator.write_record(
808 size_dist_info.git_rev, code_size_text, output)
809
810 def gen_code_size_comparison(self) -> None:
811 """Generate results of code size changes between two Git revisions,
812 old and new.
813
814 - Measured code size result of these two Git revisions must be available.
815 - The result is directed into either file / stdout depending on
816 the option, size_common_info.result_options.stdout. (Default: file)
817 """
818
819 self.logger.info("Start to generate comparision result between "\
820 "{} and {}."
821 .format(self.old_size_dist_info.git_rev,
822 self.new_size_dist_info.git_rev))
823 if self.result_options.stdout:
824 output = sys.stdout
825 else:
826 output_file = os.path.join(
827 self.comp_dir,
828 '{}-{}-{}.{}'
829 .format(self.old_size_dist_info.get_info_indication(),
830 self.new_size_dist_info.get_info_indication(),
831 self.size_common_info.get_info_indication(),
832 'md' if self.result_options.with_markdown else 'csv'))
833 output = open(output_file, "w")
834
835 self.logger.debug("Generating comparison results between {} and {}."
836 .format(self.old_size_dist_info.git_rev,
837 self.new_size_dist_info.git_rev))
838 if self.result_options.with_markdown or self.result_options.stdout:
839 print("Measure code size between {} and {} by `{}`."
840 .format(self.old_size_dist_info.get_info_indication(),
841 self.new_size_dist_info.get_info_indication(),
842 self.size_common_info.get_info_indication()),
843 file=output)
844 self.code_size_generator.write_comparison(
845 self.old_size_dist_info.git_rev,
846 self.new_size_dist_info.git_rev,
847 output, self.result_options.with_markdown,
848 self.result_options.show_all)
849
850 def get_comparision_results(self) -> None:
851 """Compare size of library/*.o between self.old_size_dist_info and
852 self.old_size_dist_info and generate the result file."""
853 build_tree.check_repo_path()
854 self.gen_code_size_report(self.old_size_dist_info)
855 self.gen_code_size_report(self.new_size_dist_info)
856 self.gen_code_size_comparison()
857
858def main():
859 parser = argparse.ArgumentParser(description=(__doc__))
860 group_required = parser.add_argument_group(
861 'required arguments',
862 'required arguments to parse for running ' + os.path.basename(__file__))
863 group_required.add_argument(
864 '-o', '--old-rev', type=str, required=True,
865 help='old Git revision for comparison.')
866
867 group_optional = parser.add_argument_group(
868 'optional arguments',
869 'optional arguments to parse for running ' + os.path.basename(__file__))
870 group_optional.add_argument(
871 '--record-dir', type=str, default='code_size_records',
872 help='directory where code size record is stored. '
873 '(Default: code_size_records)')
874 group_optional.add_argument(
875 '--comp-dir', type=str, default='comparison',
876 help='directory where comparison result is stored. '
877 '(Default: comparison)')
878 group_optional.add_argument(
879 '-n', '--new-rev', type=str, default='current',
880 help='new Git revision as comparison base. '
881 '(Default is the current work directory, including uncommitted '
882 'changes.)')
883 group_optional.add_argument(
884 '-a', '--arch', type=str, default=detect_arch(),
885 choices=list(map(lambda s: s.value, SupportedArch)),
886 help='Specify architecture for code size comparison. '
887 '(Default is the host architecture.)')
888 group_optional.add_argument(
889 '-c', '--config', type=str, default=SupportedConfig.DEFAULT.value,
890 choices=list(map(lambda s: s.value, SupportedConfig)),
891 help='Specify configuration type for code size comparison. '
892 '(Default is the current Mbed TLS configuration.)')
893 group_optional.add_argument(
894 '--markdown', action='store_true', dest='markdown',
895 help='Show comparision of code size in a markdown table. '
896 '(Only show the files that have changed).')
897 group_optional.add_argument(
898 '--stdout', action='store_true', dest='stdout',
899 help='Set this option to direct comparison result into sys.stdout. '
900 '(Default: file)')
901 group_optional.add_argument(
902 '--show-all', action='store_true', dest='show_all',
903 help='Show all the objects in comparison result, including the ones '
904 'that haven\'t changed in code size. (Default: False)')
905 group_optional.add_argument(
906 '--verbose', action='store_true', dest='verbose',
907 help='Show logs in detail for code size measurement. '
908 '(Default: False)')
909 comp_args = parser.parse_args()
910
911 logger = logging.getLogger()
912 logging_util.configure_logger(logger, split_level=logging.NOTSET)
913 logger.setLevel(logging.DEBUG if comp_args.verbose else logging.INFO)
914
915 if os.path.isfile(comp_args.record_dir):
916 logger.error("record directory: {} is not a directory"
917 .format(comp_args.record_dir))
918 sys.exit(1)
919 if os.path.isfile(comp_args.comp_dir):
920 logger.error("comparison directory: {} is not a directory"
921 .format(comp_args.comp_dir))
922 sys.exit(1)
923
924 comp_args.old_rev = CodeSizeCalculator.validate_git_revision(
925 comp_args.old_rev)
926 if comp_args.new_rev != 'current':
927 comp_args.new_rev = CodeSizeCalculator.validate_git_revision(
928 comp_args.new_rev)
929
930 # version, git_rev, arch, config, compiler, opt_level
931 old_size_dist_info = CodeSizeDistinctInfo(
932 'old', comp_args.old_rev, comp_args.arch, comp_args.config, 'cc', '-Os')
933 new_size_dist_info = CodeSizeDistinctInfo(
934 'new', comp_args.new_rev, comp_args.arch, comp_args.config, 'cc', '-Os')
935 # host_arch, measure_cmd
936 size_common_info = CodeSizeCommonInfo(
937 detect_arch(), 'size -t')
938 # record_dir, comp_dir, with_markdown, stdout, show_all
939 result_options = CodeSizeResultInfo(
940 comp_args.record_dir, comp_args.comp_dir,
941 comp_args.markdown, comp_args.stdout, comp_args.show_all)
942
943 logger.info("Measure code size between {} and {} by `{}`."
944 .format(old_size_dist_info.get_info_indication(),
945 new_size_dist_info.get_info_indication(),
946 size_common_info.get_info_indication()))
947 CodeSizeComparison(old_size_dist_info, new_size_dist_info,
948 size_common_info, result_options,
949 logger).get_comparision_results()
950
951if __name__ == "__main__":
952 main()