sizes.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. #!/usr/bin/env python3
  2. # Copyright 2019 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """A tool to extract size information for chrome.
  6. For a list of command-line options, call this script with '--help'.
  7. This script uses Python 2 due to dependence on tracing.value.
  8. """
  9. from __future__ import print_function
  10. import argparse
  11. import errno
  12. import glob
  13. import json
  14. import platform
  15. import os
  16. import re
  17. import stat
  18. import subprocess
  19. import sys
  20. import tempfile
  21. SRC_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
  22. # Add Catapult to the path so we can import the chartjson-histogramset
  23. # conversion.
  24. sys.path.append(os.path.join(SRC_DIR, 'third_party', 'catapult', 'tracing'))
  25. from tracing.value import convert_chart_json
  26. sys.path.insert(0, os.path.join(SRC_DIR, 'build', 'util'))
  27. from lib.results import result_sink
  28. from lib.results import result_types
  29. class ResultsCollector:
  30. def __init__(self):
  31. self.results = {}
  32. def add_result(self, name, identifier, value, units):
  33. assert name not in self.results
  34. self.results[name] = {
  35. 'identifier': identifier,
  36. 'value': int(value),
  37. 'units': units
  38. }
  39. # Legacy printing, previously used for parsing the text logs.
  40. print('RESULT %s: %s= %s %s' % (name, identifier, value, units))
  41. def get_size(filename):
  42. return os.stat(filename)[stat.ST_SIZE]
  43. def get_linux_stripped_size(filename):
  44. EU_STRIP_NAME = 'eu-strip'
  45. # Assumes |filename| is in out/Release
  46. # build/linux/bin/eu-strip'
  47. src_dir = os.path.dirname(os.path.dirname(os.path.dirname(filename)))
  48. eu_strip_path = os.path.join(src_dir, 'build', 'linux', 'bin', EU_STRIP_NAME)
  49. if (platform.architecture()[0] == '64bit'
  50. or not os.path.exists(eu_strip_path)):
  51. eu_strip_path = EU_STRIP_NAME
  52. with tempfile.NamedTemporaryFile() as stripped_file:
  53. strip_cmd = [eu_strip_path, '-o', stripped_file.name, filename]
  54. result = 0
  55. result, _ = run_process(result, strip_cmd)
  56. if result != 0:
  57. return (result, 0)
  58. return (result, get_size(stripped_file.name))
  59. def run_process(result, command):
  60. p = subprocess.Popen(command, stdout=subprocess.PIPE)
  61. stdout = p.communicate()[0].decode()
  62. if p.returncode != 0:
  63. print('ERROR from command "%s": %d' % (' '.join(command), p.returncode))
  64. if result == 0:
  65. result = p.returncode
  66. return result, stdout
  67. def main_mac(output_directory, results_collector, size_path):
  68. """Print appropriate size information about built Mac targets.
  69. Returns the first non-zero exit status of any command it executes,
  70. or zero on success.
  71. """
  72. result = 0
  73. # Work with either build type.
  74. base_names = ('Chromium', 'Google Chrome')
  75. for base_name in base_names:
  76. app_bundle = base_name + '.app'
  77. framework_name = base_name + ' Framework'
  78. framework_bundle = framework_name + '.framework'
  79. framework_dsym_bundle = framework_name + '.dSYM'
  80. chromium_app_dir = os.path.join(output_directory, app_bundle)
  81. chromium_executable = os.path.join(chromium_app_dir, 'Contents', 'MacOS',
  82. base_name)
  83. chromium_framework_dir = os.path.join(output_directory, framework_bundle)
  84. chromium_framework_executable = os.path.join(chromium_framework_dir,
  85. framework_name)
  86. chromium_framework_dsym_dir = os.path.join(output_directory,
  87. framework_dsym_bundle)
  88. chromium_framework_dsym = os.path.join(chromium_framework_dsym_dir,
  89. 'Contents', 'Resources', 'DWARF',
  90. framework_name)
  91. if os.path.exists(chromium_executable):
  92. print_dict = {
  93. # Remove spaces in the names so any downstream processing is less
  94. # likely to choke.
  95. 'app_name': re.sub(r'\s', '', base_name),
  96. 'app_bundle': re.sub(r'\s', '', app_bundle),
  97. 'framework_name': re.sub(r'\s', '', framework_name),
  98. 'framework_bundle': re.sub(r'\s', '', framework_bundle),
  99. 'app_size': get_size(chromium_executable),
  100. 'framework_size': get_size(chromium_framework_executable),
  101. 'framework_dsym_name': re.sub(r'\s', '', framework_name) + 'Dsym',
  102. 'framework_dsym_size': get_size(chromium_framework_dsym),
  103. }
  104. # Collect the segment info out of the App
  105. result, stdout = run_process(result, [size_path, chromium_executable])
  106. print_dict['app_text'], print_dict['app_data'], print_dict['app_objc'] = \
  107. re.search(r'(\d+)\s+(\d+)\s+(\d+)', stdout).groups()
  108. # Collect the segment info out of the Framework
  109. result, stdout = run_process(result,
  110. [size_path, chromium_framework_executable])
  111. print_dict['framework_text'], print_dict['framework_data'], \
  112. print_dict['framework_objc'] = \
  113. re.search(r'(\d+)\s+(\d+)\s+(\d+)', stdout).groups()
  114. # Collect the whole size of the App bundle on disk (include the framework)
  115. whole_size = 0
  116. for root_dir, _, filenames in os.walk(chromium_app_dir,
  117. followlinks=False):
  118. for filename in filenames:
  119. full_path = os.path.join(root_dir, filename)
  120. if not os.path.islink(full_path):
  121. whole_size += get_size(full_path)
  122. print_dict['app_bundle_size'] = whole_size
  123. results_collector.add_result(print_dict['app_name'],
  124. print_dict['app_name'],
  125. print_dict['app_size'], 'bytes')
  126. results_collector.add_result('%s-__TEXT' % print_dict['app_name'],
  127. '__TEXT', print_dict['app_text'], 'bytes')
  128. results_collector.add_result('%s-__DATA' % print_dict['app_name'],
  129. '__DATA', print_dict['app_data'], 'bytes')
  130. results_collector.add_result('%s-__OBJC' % print_dict['app_name'],
  131. '__OBJC', print_dict['app_objc'], 'bytes')
  132. results_collector.add_result(print_dict['framework_name'],
  133. print_dict['framework_name'],
  134. print_dict['framework_size'], 'bytes')
  135. results_collector.add_result('%s-__TEXT' % print_dict['framework_name'],
  136. '__TEXT', print_dict['framework_text'],
  137. 'bytes')
  138. results_collector.add_result('%s-__DATA' % print_dict['framework_name'],
  139. '__DATA', print_dict['framework_data'],
  140. 'bytes')
  141. results_collector.add_result('%s-__OBJC' % print_dict['framework_name'],
  142. '__OBJC', print_dict['framework_objc'],
  143. 'bytes')
  144. results_collector.add_result(print_dict['app_bundle'],
  145. print_dict['app_bundle'],
  146. print_dict['app_bundle_size'], 'bytes')
  147. results_collector.add_result(print_dict['framework_dsym_name'],
  148. print_dict['framework_dsym_name'],
  149. print_dict['framework_dsym_size'], 'bytes')
  150. # Found a match, don't check the other base_names.
  151. return result
  152. # If no base_names matched, fail script.
  153. return 66
  154. def check_linux_binary(binary_name, output_directory):
  155. """Collect appropriate size information about the built Linux binary given.
  156. Returns a tuple (result, sizes). result is the first non-zero exit
  157. status of any command it executes, or zero on success. sizes is a list
  158. of tuples (name, identifier, totals_identifier, value, units).
  159. The printed line looks like:
  160. name: identifier= value units
  161. When this same data is used for totals across all the binaries, then
  162. totals_identifier is the identifier to use, or '' to just use identifier.
  163. """
  164. binary_file = os.path.join(output_directory, binary_name)
  165. if not os.path.exists(binary_file):
  166. # Don't print anything for missing files.
  167. return 0, []
  168. result = 0
  169. sizes = []
  170. sizes.append((binary_name, binary_name, 'size', get_size(binary_file),
  171. 'bytes'))
  172. result, stripped_size = get_linux_stripped_size(binary_file)
  173. sizes.append((binary_name + '-stripped', 'stripped', 'stripped',
  174. stripped_size, 'bytes'))
  175. result, stdout = run_process(result, ['size', binary_file])
  176. text, data, bss = re.search(r'(\d+)\s+(\d+)\s+(\d+)', stdout).groups()
  177. sizes += [
  178. (binary_name + '-text', 'text', '', text, 'bytes'),
  179. (binary_name + '-data', 'data', '', data, 'bytes'),
  180. (binary_name + '-bss', 'bss', '', bss, 'bytes'),
  181. ]
  182. # Determine if the binary has the DT_TEXTREL marker.
  183. result, stdout = run_process(result, ['readelf', '-Wd', binary_file])
  184. if re.search(r'\bTEXTREL\b', stdout) is None:
  185. # Nope, so the count is zero.
  186. count = 0
  187. else:
  188. # There are some, so count them.
  189. result, stdout = run_process(result, ['eu-findtextrel', binary_file])
  190. count = stdout.count('\n')
  191. sizes.append((binary_name + '-textrel', 'textrel', '', count, 'relocs'))
  192. return result, sizes
  193. def main_linux(output_directory, results_collector, size_path):
  194. """Print appropriate size information about built Linux targets.
  195. Returns the first non-zero exit status of any command it executes,
  196. or zero on success.
  197. """
  198. assert size_path is None
  199. binaries = [
  200. 'chrome',
  201. 'nacl_helper',
  202. 'nacl_helper_bootstrap',
  203. 'libffmpegsumo.so',
  204. 'libgcflashplayer.so',
  205. 'libppGoogleNaClPluginChrome.so',
  206. ]
  207. result = 0
  208. totals = {}
  209. for binary in binaries:
  210. this_result, this_sizes = check_linux_binary(binary, output_directory)
  211. if result == 0:
  212. result = this_result
  213. for name, identifier, totals_id, value, units in this_sizes:
  214. results_collector.add_result(name, identifier, value, units)
  215. totals_id = totals_id or identifier, units
  216. totals[totals_id] = totals.get(totals_id, 0) + int(value)
  217. files = [
  218. 'nacl_irt_x86_64.nexe',
  219. 'resources.pak',
  220. ]
  221. for filename in files:
  222. path = os.path.join(output_directory, filename)
  223. try:
  224. size = get_size(path)
  225. except OSError as e:
  226. if e.errno == errno.ENOENT:
  227. continue # Don't print anything for missing files.
  228. raise
  229. results_collector.add_result(filename, filename, size, 'bytes')
  230. totals['size', 'bytes'] += size
  231. # TODO(mcgrathr): This should all be refactored so the mac and win flavors
  232. # also deliver data structures rather than printing, and the logic for
  233. # the printing and the summing totals is shared across all three flavors.
  234. for (identifier, units), value in sorted(totals.items()):
  235. results_collector.add_result('totals-%s' % identifier, identifier, value,
  236. units)
  237. return result
  238. def check_android_binaries(binaries,
  239. output_directory,
  240. results_collector,
  241. binaries_to_print=None):
  242. """Common method for printing size information for Android targets.
  243. Prints size information for each element of binaries in the output
  244. directory. If binaries_to_print is specified, the name of each binary from
  245. binaries is replaced with corresponding element of binaries_to_print
  246. in output. Returns the first non-zero exit status of any command it
  247. executes, or zero on success.
  248. """
  249. result = 0
  250. if not binaries_to_print:
  251. binaries_to_print = binaries
  252. for (binary, binary_to_print) in zip(binaries, binaries_to_print):
  253. this_result, this_sizes = check_linux_binary(binary, output_directory)
  254. if result == 0:
  255. result = this_result
  256. for name, identifier, _, value, units in this_sizes:
  257. name = name.replace('/', '_').replace(binary, binary_to_print)
  258. identifier = identifier.replace(binary, binary_to_print)
  259. results_collector.add_result(name, identifier, value, units)
  260. return result
  261. def main_android(output_directory, results_collector, size_path):
  262. """Print appropriate size information about built Android targets.
  263. Returns the first non-zero exit status of any command it executes,
  264. or zero on success.
  265. """
  266. assert size_path is None
  267. binaries = [
  268. 'chrome_public_apk/libs/armeabi-v7a/libchrome.so',
  269. 'lib/libchrome.so',
  270. 'libchrome.so',
  271. ]
  272. return check_android_binaries(binaries, output_directory, results_collector)
  273. def main_android_cronet(output_directory, results_collector, size_path):
  274. """Print appropriate size information about Android Cronet targets.
  275. Returns the first non-zero exit status of any command it executes,
  276. or zero on success.
  277. """
  278. assert size_path is None
  279. # Use version in binary file name, but not in printed output.
  280. binaries_with_paths = glob.glob(
  281. os.path.join(output_directory, 'libcronet.*.so'))
  282. num_binaries = len(binaries_with_paths)
  283. assert num_binaries == 1, "Got %d binaries: %s" % (
  284. num_binaries, ', '.join(binaries_with_paths))
  285. binaries = [os.path.basename(binaries_with_paths[0])]
  286. binaries_to_print = ['libcronet.so']
  287. return check_android_binaries(binaries, output_directory, results_collector,
  288. binaries_to_print)
  289. def main_win(output_directory, results_collector, size_path):
  290. """Print appropriate size information about built Windows targets.
  291. Returns the first non-zero exit status of any command it executes,
  292. or zero on success.
  293. """
  294. assert size_path is None
  295. files = [
  296. 'chrome.dll',
  297. 'chrome.dll.pdb',
  298. 'chrome.exe',
  299. 'chrome_child.dll',
  300. 'chrome_child.dll.pdb',
  301. 'chrome_elf.dll',
  302. 'chrome_proxy.exe',
  303. 'chrome_watcher.dll',
  304. 'elevation_service.exe',
  305. 'libEGL.dll',
  306. 'libGLESv2.dll',
  307. 'mini_installer.exe',
  308. 'notification_helper.exe',
  309. 'resources.pak',
  310. 'setup.exe',
  311. 'WidevineCdm\\_platform_specific\\win_x64\\widevinecdm.dll',
  312. 'WidevineCdm\\_platform_specific\\win_x64\\widevinecdmadapter.dll',
  313. 'WidevineCdm\\_platform_specific\\win_x86\\widevinecdm.dll',
  314. 'WidevineCdm\\_platform_specific\\win_x86\\widevinecdmadapter.dll',
  315. ]
  316. for f in files:
  317. p = os.path.join(output_directory, f)
  318. if os.path.isfile(p):
  319. results_collector.add_result(f, f, get_size(p), 'bytes')
  320. return 0
  321. def format_for_histograms_conversion(data):
  322. # We need to do two things to the provided data to make it compatible with the
  323. # conversion script:
  324. # 1. Add a top-level "benchmark_name" key.
  325. # 2. Pull out the "identifier" value to be the story name.
  326. formatted_data = {}
  327. for metric, metric_data in data.items():
  328. story = metric_data['identifier']
  329. formatted_data[metric] = {story: metric_data.copy()}
  330. del formatted_data[metric][story]['identifier']
  331. return {'benchmark_name': 'sizes', 'charts': formatted_data}
  332. def main():
  333. if sys.platform in ('win32', 'cygwin'):
  334. default_platform = 'win'
  335. elif sys.platform.startswith('darwin'):
  336. default_platform = 'mac'
  337. elif sys.platform.startswith('linux'):
  338. default_platform = 'linux'
  339. else:
  340. default_platform = None
  341. main_map = {
  342. 'android': main_android,
  343. 'android-cronet': main_android_cronet,
  344. 'linux': main_linux,
  345. 'mac': main_mac,
  346. 'win': main_win,
  347. }
  348. platforms = sorted(main_map.keys())
  349. parser = argparse.ArgumentParser()
  350. parser.add_argument(
  351. '--output-directory',
  352. type=os.path.realpath,
  353. help='Chromium output directory, e.g. /path/to/src/out/Debug')
  354. parser.add_argument(
  355. '--platform',
  356. default=default_platform,
  357. help='specify platform (%s) [default: %%(default)s]' %
  358. ', '.join(platforms))
  359. parser.add_argument('--size-path', default=None, help='Path to size binary')
  360. # Accepted to conform to the isolated script interface, but ignored.
  361. parser.add_argument('--isolated-script-test-filter', help=argparse.SUPPRESS)
  362. parser.add_argument(
  363. '--isolated-script-test-perf-output', help=argparse.SUPPRESS)
  364. parser.add_argument(
  365. '--isolated-script-test-output',
  366. type=os.path.realpath,
  367. help='File to which simplified JSON results will be written.')
  368. args = parser.parse_args()
  369. real_main = main_map.get(args.platform)
  370. if not real_main:
  371. if args.platform is None:
  372. sys.stderr.write('Unsupported sys.platform %s.\n' % repr(sys.platform))
  373. else:
  374. sys.stderr.write('Unknown platform %s.\n' % repr(args.platform))
  375. msg = 'Use the --platform= option to specify a supported platform:\n'
  376. sys.stderr.write(msg + ' ' + ' '.join(platforms) + '\n')
  377. return 2
  378. isolated_script_output = {
  379. 'valid': False,
  380. 'failures': [],
  381. 'version': 'simplified'
  382. }
  383. test_name = 'sizes'
  384. results_directory = None
  385. if args.isolated_script_test_output:
  386. results_directory = os.path.join(
  387. os.path.dirname(args.isolated_script_test_output), test_name)
  388. if not os.path.exists(results_directory):
  389. os.makedirs(results_directory)
  390. results_collector = ResultsCollector()
  391. result_sink_client = result_sink.TryInitClient()
  392. try:
  393. rc = real_main(args.output_directory, results_collector, args.size_path)
  394. isolated_script_output = {
  395. 'valid': True,
  396. 'failures': [test_name] if rc else [],
  397. 'version': 'simplified',
  398. }
  399. finally:
  400. if results_directory:
  401. results_path = os.path.join(results_directory, 'test_results.json')
  402. with open(results_path, 'w') as output_file:
  403. json.dump(isolated_script_output, output_file)
  404. histogram_path = os.path.join(results_directory, 'perf_results.json')
  405. # We need to add a bit more data to the results and rearrange some things,
  406. # otherwise the conversion fails due to the provided data being malformed.
  407. updated_results = format_for_histograms_conversion(
  408. results_collector.results)
  409. with open(histogram_path, 'w') as f:
  410. json.dump(updated_results, f)
  411. histogram_result = convert_chart_json.ConvertChartJson(histogram_path)
  412. if histogram_result.returncode != 0:
  413. sys.stderr.write(
  414. 'chartjson conversion failed: %s\n' % histogram_result.stdout)
  415. rc = rc or histogram_result.returncode
  416. else:
  417. with open(histogram_path, 'wb') as f:
  418. f.write(histogram_result.stdout)
  419. if result_sink_client:
  420. status = result_types.PASS
  421. if not isolated_script_output['valid']:
  422. status = result_types.UNKNOWN
  423. elif isolated_script_output['failures']:
  424. status = result_types.FAIL
  425. result_sink_client.Post(test_name, status, None, None, None)
  426. return rc
  427. if '__main__' == __name__:
  428. sys.exit(main())