diff options
Diffstat (limited to 'chromium/infra/scripts/sizes.py')
-rwxr-xr-x | chromium/infra/scripts/sizes.py | 523 |
1 files changed, 523 insertions, 0 deletions
diff --git a/chromium/infra/scripts/sizes.py b/chromium/infra/scripts/sizes.py new file mode 100755 index 00000000000..0106772a131 --- /dev/null +++ b/chromium/infra/scripts/sizes.py @@ -0,0 +1,523 @@ +#!/usr/bin/env python +# Copyright (c) 2012 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""A tool to extract size information for chrome, executed by buildbot. + +When this is run, the current directory (cwd) should be the outer build +directory (e.g., chrome-release/build/). + +For a list of command-line options, call this script with '--help'. +""" + +import errno +import glob +import json +import platform +import optparse +import os +import re +import stat +import subprocess +import sys +import tempfile + +import build_directory + + +SRC_DIR = os.path.abspath( + os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', '..', '..')) + +# Add Catapult to the path so we can import the chartjson-histogramset +# conversion. +sys.path.append(os.path.join(SRC_DIR, 'third_party', 'catapult', 'tracing')) +from tracing.value import convert_chart_json + +class ResultsCollector(object): + def __init__(self): + self.results = {} + + def add_result(self, name, identifier, value, units): + assert name not in self.results + self.results[name] = { + 'identifier': identifier, + 'value': int(value), + 'units': units + } + + # Legacy printing, previously used for parsing the text logs. + print 'RESULT %s: %s= %s %s' % (name, identifier, value, units) + + +def get_size(filename): + return os.stat(filename)[stat.ST_SIZE] + + +def get_linux_stripped_size(filename): + EU_STRIP_NAME = 'eu-strip' + # Assumes |filename| is in out/Release + # build/linux/bin/eu-strip' + src_dir = os.path.dirname(os.path.dirname(os.path.dirname(filename))) + eu_strip_path = os.path.join(src_dir, 'build', 'linux', 'bin', EU_STRIP_NAME) + if (platform.architecture()[0] == '64bit' or + not os.path.exists(eu_strip_path)): + eu_strip_path = EU_STRIP_NAME + + with tempfile.NamedTemporaryFile() as stripped_file: + strip_cmd = [eu_strip_path, '-o', stripped_file.name, filename] + result = 0 + result, _ = run_process(result, strip_cmd) + if result != 0: + return (result, 0) + return (result, get_size(stripped_file.name)) + + +def run_process(result, command): + p = subprocess.Popen(command, stdout=subprocess.PIPE) + stdout = p.communicate()[0] + if p.returncode != 0: + print 'ERROR from command "%s": %d' % (' '.join(command), p.returncode) + if result == 0: + result = p.returncode + return result, stdout + + +def main_mac(options, args, results_collector): + """Print appropriate size information about built Mac targets. + + Returns the first non-zero exit status of any command it executes, + or zero on success. + """ + build_dir = build_directory.GetBuildOutputDirectory(SRC_DIR) + target_dir = os.path.join(build_dir, options.target) + + size_path = 'size' + + # If there's a hermetic download of Xcode, directly invoke 'size' from it. The + # hermetic xcode binaries aren't a full Xcode install, so we can't modify + # DEVELOPER_DIR. + hermetic_size_path = os.path.join( + SRC_DIR, 'build', 'mac_files', 'xcode_binaries', 'Contents', + 'Developer', 'Toolchains', 'XcodeDefault.xctoolchain', 'usr', 'bin', + 'size') + if os.path.exists(hermetic_size_path): + size_path = hermetic_size_path + + result = 0 + # Work with either build type. + base_names = ('Chromium', 'Google Chrome') + for base_name in base_names: + app_bundle = base_name + '.app' + framework_name = base_name + ' Framework' + framework_bundle = framework_name + '.framework' + framework_dsym_bundle = framework_name + '.dSYM' + framework_unstripped_name = framework_name + '.unstripped' + + chromium_app_dir = os.path.join(target_dir, app_bundle) + chromium_executable = os.path.join(chromium_app_dir, + 'Contents', 'MacOS', base_name) + + chromium_framework_dir = os.path.join(target_dir, framework_bundle) + chromium_framework_executable = os.path.join(chromium_framework_dir, + framework_name) + + chromium_framework_dsym_dir = os.path.join(target_dir, + framework_dsym_bundle) + chromium_framework_dsym = os.path.join(chromium_framework_dsym_dir, + 'Contents', 'Resources', 'DWARF', + framework_name) + chromium_framework_unstripped = os.path.join(target_dir, + framework_unstripped_name) + if os.path.exists(chromium_executable): + print_dict = { + # Remove spaces in the names so any downstream processing is less + # likely to choke. + 'app_name' : re.sub(r'\s', '', base_name), + 'app_bundle' : re.sub(r'\s', '', app_bundle), + 'framework_name' : re.sub(r'\s', '', framework_name), + 'framework_bundle' : re.sub(r'\s', '', framework_bundle), + 'app_size' : get_size(chromium_executable), + 'framework_size' : get_size(chromium_framework_executable), + 'framework_dsym_name' : re.sub(r'\s', '', framework_name) + 'Dsym', + 'framework_dsym_size' : get_size(chromium_framework_dsym), + } + + # Collect the segment info out of the App + result, stdout = run_process(result, [size_path, chromium_executable]) + print_dict['app_text'], print_dict['app_data'], print_dict['app_objc'] = \ + re.search(r'(\d+)\s+(\d+)\s+(\d+)', stdout).groups() + + # Collect the segment info out of the Framework + result, stdout = run_process(result, [size_path, + chromium_framework_executable]) + print_dict['framework_text'], print_dict['framework_data'], \ + print_dict['framework_objc'] = \ + re.search(r'(\d+)\s+(\d+)\s+(\d+)', stdout).groups() + + # Collect the whole size of the App bundle on disk (include the framework) + result, stdout = run_process(result, ['du', '-s', '-k', chromium_app_dir]) + du_s = re.search(r'(\d+)', stdout).group(1) + print_dict['app_bundle_size'] = (int(du_s) * 1024) + + results_collector.add_result( + print_dict['app_name'], print_dict['app_name'], + print_dict['app_size'], 'bytes') + results_collector.add_result( + '%s-__TEXT' % print_dict['app_name'], '__TEXT', + print_dict['app_text'], 'bytes') + results_collector.add_result( + '%s-__DATA' % print_dict['app_name'], '__DATA', + print_dict['app_data'], 'bytes') + results_collector.add_result( + '%s-__OBJC' % print_dict['app_name'], '__OBJC', + print_dict['app_objc'], 'bytes') + results_collector.add_result( + print_dict['framework_name'], print_dict['framework_name'], + print_dict['framework_size'], 'bytes') + results_collector.add_result( + '%s-__TEXT' % print_dict['framework_name'], '__TEXT', + print_dict['framework_text'], 'bytes') + results_collector.add_result( + '%s-__DATA' % print_dict['framework_name'], '__DATA', + print_dict['framework_data'], 'bytes') + results_collector.add_result( + '%s-__OBJC' % print_dict['framework_name'], '__OBJC', + print_dict['framework_objc'], 'bytes') + results_collector.add_result( + print_dict['app_bundle'], print_dict['app_bundle'], + print_dict['app_bundle_size'], 'bytes') + results_collector.add_result( + print_dict['framework_dsym_name'], print_dict['framework_dsym_name'], + print_dict['framework_dsym_size'], 'bytes') + + # Found a match, don't check the other base_names. + return result + # If no base_names matched, fail script. + return 66 + + +def check_linux_binary(target_dir, binary_name, options, results_collector): + """Collect appropriate size information about the built Linux binary given. + + Returns a tuple (result, sizes). result is the first non-zero exit + status of any command it executes, or zero on success. sizes is a list + of tuples (name, identifier, totals_identifier, value, units). + The printed line looks like: + name: identifier= value units + When this same data is used for totals across all the binaries, then + totals_identifier is the identifier to use, or '' to just use identifier. + """ + binary_file = os.path.join(target_dir, binary_name) + + if not os.path.exists(binary_file): + # Don't print anything for missing files. + return 0, [] + + result = 0 + sizes = [] + + sizes.append((binary_name, binary_name, 'size', + get_size(binary_file), 'bytes')) + + + result, stripped_size = get_linux_stripped_size(binary_file) + sizes.append((binary_name + '-stripped', 'stripped', 'stripped', + stripped_size, 'bytes')) + + result, stdout = run_process(result, ['size', binary_file]) + text, data, bss = re.search(r'(\d+)\s+(\d+)\s+(\d+)', stdout).groups() + sizes += [ + (binary_name + '-text', 'text', '', text, 'bytes'), + (binary_name + '-data', 'data', '', data, 'bytes'), + (binary_name + '-bss', 'bss', '', bss, 'bytes'), + ] + + # Determine if the binary has the DT_TEXTREL marker. + result, stdout = run_process(result, ['readelf', '-Wd', binary_file]) + if re.search(r'\bTEXTREL\b', stdout) is None: + # Nope, so the count is zero. + count = 0 + else: + # There are some, so count them. + result, stdout = run_process(result, ['eu-findtextrel', binary_file]) + count = stdout.count('\n') + sizes.append((binary_name + '-textrel', 'textrel', '', count, 'relocs')) + + return result, sizes + + +def main_linux(options, args, results_collector): + """Print appropriate size information about built Linux targets. + + Returns the first non-zero exit status of any command it executes, + or zero on success. + """ + build_dir = build_directory.GetBuildOutputDirectory(SRC_DIR) + target_dir = os.path.join(build_dir, options.target) + + binaries = [ + 'chrome', + 'nacl_helper', + 'nacl_helper_bootstrap', + 'libffmpegsumo.so', + 'libgcflashplayer.so', + 'libppGoogleNaClPluginChrome.so', + ] + + result = 0 + + totals = {} + + for binary in binaries: + this_result, this_sizes = check_linux_binary(target_dir, binary, options, + results_collector) + if result == 0: + result = this_result + for name, identifier, totals_id, value, units in this_sizes: + results_collector.add_result(name, identifier, value, units) + totals_id = totals_id or identifier, units + totals[totals_id] = totals.get(totals_id, 0) + int(value) + + files = [ + 'nacl_irt_x86_64.nexe', + 'resources.pak', + ] + + for filename in files: + path = os.path.join(target_dir, filename) + try: + size = get_size(path) + except OSError, e: + if e.errno == errno.ENOENT: + continue # Don't print anything for missing files. + raise + results_collector.add_result(filename, filename, size, 'bytes') + totals['size', 'bytes'] += size + + # TODO(mcgrathr): This should all be refactored so the mac and win flavors + # also deliver data structures rather than printing, and the logic for + # the printing and the summing totals is shared across all three flavors. + for (identifier, units), value in sorted(totals.iteritems()): + results_collector.add_result( + 'totals-%s' % identifier, identifier, value, units) + + return result + + +def check_android_binaries(binaries, target_dir, options, results_collector, + binaries_to_print=None): + """Common method for printing size information for Android targets. + + Prints size information for each element of binaries in target_dir. + If binaries_to_print is specified, the name of each binary from + binaries is replaced with corresponding element of binaries_to_print + in output. Returns the first non-zero exit status of any command it + executes, or zero on success. + """ + result = 0 + if not binaries_to_print: + binaries_to_print = binaries + + for (binary, binary_to_print) in zip(binaries, binaries_to_print): + this_result, this_sizes = check_linux_binary(target_dir, binary, options, + results_collector) + if result == 0: + result = this_result + for name, identifier, _, value, units in this_sizes: + name = name.replace('/', '_').replace(binary, binary_to_print) + identifier = identifier.replace(binary, binary_to_print) + results_collector.add_result(name, identifier, value, units) + + return result + + +def main_android(options, args, results_collector): + """Print appropriate size information about built Android targets. + + Returns the first non-zero exit status of any command it executes, + or zero on success. + """ + target_dir = os.path.join(build_directory.GetBuildOutputDirectory(SRC_DIR), + options.target) + + binaries = [ + 'chrome_public_apk/libs/armeabi-v7a/libchrome.so', + 'lib/libchrome.so', + 'libchrome.so', + ] + + return check_android_binaries(binaries, target_dir, options, + results_collector) + + +def main_android_webview(options, args, results_collector): + """Print appropriate size information about Android WebViewChromium targets. + + Returns the first non-zero exit status of any command it executes, + or zero on success. + """ + target_dir = os.path.join(build_directory.GetBuildOutputDirectory(SRC_DIR), + options.target) + + binaries = ['lib/libwebviewchromium.so', + 'libwebviewchromium.so'] + + return check_android_binaries(binaries, target_dir, options, + results_collector) + + +def main_android_cronet(options, args, results_collector): + """Print appropriate size information about Android Cronet targets. + + Returns the first non-zero exit status of any command it executes, + or zero on success. + """ + target_dir = os.path.join(build_directory.GetBuildOutputDirectory(SRC_DIR), + options.target) + # Use version in binary file name, but not in printed output. + binaries_with_paths = glob.glob(os.path.join(target_dir,'libcronet.*.so')) + num_binaries = len(binaries_with_paths) + assert num_binaries == 1, "Got %d binaries" % (num_binaries,) + binaries = [os.path.basename(binaries_with_paths[0])] + binaries_to_print = ['libcronet.so'] + + return check_android_binaries(binaries, target_dir, options, + results_collector, binaries_to_print) + + +def main_win(options, args, results_collector): + """Print appropriate size information about built Windows targets. + + Returns the first non-zero exit status of any command it executes, + or zero on success. + """ + files = [ + 'chrome.dll', + 'chrome.dll.pdb', + 'chrome.exe', + 'chrome_child.dll', + 'chrome_child.dll.pdb', + 'chrome_elf.dll', + 'chrome_watcher.dll', + 'libEGL.dll', + 'libGLESv2.dll', + 'mini_installer.exe', + 'resources.pak', + 'setup.exe', + 'swiftshader\\libEGL.dll', + 'swiftshader\\libGLESv2.dll', + 'WidevineCdm\\_platform_specific\\win_x64\\widevinecdm.dll', + 'WidevineCdm\\_platform_specific\\win_x64\\widevinecdmadapter.dll', + 'WidevineCdm\\_platform_specific\\win_x86\\widevinecdm.dll', + 'WidevineCdm\\_platform_specific\\win_x86\\widevinecdmadapter.dll', + ] + + build_dir = build_directory.GetBuildOutputDirectory(SRC_DIR) + target_dir = os.path.join(build_dir, options.target) + + for f in files: + p = os.path.join(target_dir, f) + if os.path.isfile(p): + results_collector.add_result(f, f, get_size(p), 'bytes') + + return 0 + + +def format_for_histograms_conversion(data): + # We need to do two things to the provided data to make it compatible with the + # conversion script: + # 1. Add a top-level "benchmark_name" key. + # 2. Pull out the "identifier" value to be the story name. + formatted_data = {} + for metric, metric_data in data.iteritems(): + story = metric_data['identifier'] + formatted_data[metric] = { + story: metric_data.copy() + } + del formatted_data[metric][story]['identifier'] + return { + 'benchmark_name': 'sizes', + 'charts': formatted_data + } + + +def main(): + if sys.platform in ('win32', 'cygwin'): + default_platform = 'win' + elif sys.platform.startswith('darwin'): + default_platform = 'mac' + elif sys.platform == 'linux2': + default_platform = 'linux' + else: + default_platform = None + + main_map = { + 'android' : main_android, + 'android-webview' : main_android_webview, + 'android-cronet' : main_android_cronet, + 'linux' : main_linux, + 'mac' : main_mac, + 'win' : main_win, + } + platforms = sorted(main_map.keys()) + + option_parser = optparse.OptionParser() + option_parser.add_option('--target', + default='Release', + help='build target (Debug, Release) ' + '[default: %default]') + option_parser.add_option('--target-dir', help='ignored') + option_parser.add_option('--build-dir', help='ignored') + option_parser.add_option('--platform', + default=default_platform, + help='specify platform (%s) [default: %%default]' + % ', '.join(platforms)) + option_parser.add_option('--json', help='Path to JSON output file') + # This needs to be --output-dir (and not something like --output-directory) in + # order to work properly with the build-side runtest.py script that's + # currently used for dashboard uploading results from this script. + option_parser.add_option('--output-dir', + help='Directory to dump data in the HistogramSet ' + 'format') + + options, args = option_parser.parse_args() + + real_main = main_map.get(options.platform) + if not real_main: + if options.platform is None: + sys.stderr.write('Unsupported sys.platform %s.\n' % repr(sys.platform)) + else: + sys.stderr.write('Unknown platform %s.\n' % repr(options.platform)) + msg = 'Use the --platform= option to specify a supported platform:\n' + sys.stderr.write(msg + ' ' + ' '.join(platforms) + '\n') + return 2 + + results_collector = ResultsCollector() + rc = real_main(options, args, results_collector) + + if options.json: + with open(options.json, 'w') as f: + json.dump(results_collector.results, f) + + if options.output_dir: + histogram_path = os.path.join(options.output_dir, 'perf_results.json') + # We need to add a bit more data to the results and rearrange some things, + # otherwise the conversion fails due to the provided data being malformed. + updated_results = format_for_histograms_conversion( + results_collector.results) + with open(histogram_path, 'w') as f: + json.dump(updated_results, f) + histogram_result = convert_chart_json.ConvertChartJson(histogram_path) + if histogram_result.returncode != 0: + sys.stderr.write( + 'chartjson conversion failed: %s\n' % histogram_result.stdout) + return histogram_result.returncode + with open(histogram_path, 'w') as f: + f.write(histogram_result.stdout) + + return rc + + +if '__main__' == __name__: + sys.exit(main()) |