uberpoet/multisuite.py (240 lines of code) (raw):

# Copyright (c) 2018 Uber Technologies, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import, print_function import argparse import datetime import logging import shutil import subprocess import sys import tempfile import time from os.path import join from . import commandlineutil, projectgen from .cpulogger import CPULogger from .moduletree import ModuleGenType from .statemanagement import SettingsState, XcodeManager from .util import check_dependent_commands, grab_mac_marketing_name, makedir, sudo_enabled class CommandLineMultisuite(object): @staticmethod def parse_config(args): tmp_root = join(tempfile.gettempdir(), 'mock_app_gen_out') log_root = join(tmp_root, 'logs') parser = argparse.ArgumentParser( description='Build all mock app types and log times & traces to a log file. ' 'Useful as a benchmark suite to compare build performance of different computers.') parser.add_argument('--log_dir', default=log_root, help="Where logs such as build times should exist."), parser.add_argument( '--app_gen_output_dir', default=tmp_root, help="Where generated mock apps should be outputted to."), parser.add_argument('--buck_command', default='buck', help="The path to the buck binary. Defaults to `buck`."), commandlineutil.AppGenerationConfig.add_app_gen_options(parser) actions = parser.add_argument_group('Extra Actions') actions.add_argument( '--trace_cpu', action='store_true', default=False, help="If we should add cpu utilization to build traces."), actions.add_argument( '--switch_xcode_versions', action='store_true', default=False, help="Switch xcode verions as part of the full multisuite test. This will search your " "`/Applications` directory for xcode.app bundles to build with. Requires sudo."), actions.add_argument( '--full_clean', action='store_true', default=False, help="Clean all default xcode cache directories to prevent cache effects changing test results."), testing = parser.add_argument_group('Testing Shortcuts') testing.add_argument( '--skip_xcode_build', action='store_true', default=False, help="Skips building the mock apps. Useful for speeding up testing and making " "integration tests independent of non-python dependencies."), testing.add_argument( '--test_build_only', action='store_true', default=False, help="Only builds a small flat build type to create short testing loops."), out = parser.parse_args(args) commandlineutil.AppGenerationConfig.validate_app_gen_options(out) return out # noinspection PyAttributeOutsideInit def config_to_vars(self, config): self.app_gen_options = commandlineutil.AppGenerationConfig() self.app_gen_options.pull_from_args(config) self.log_dir = config.log_dir self.output_dir = config.app_gen_output_dir self.buck_binary = config.buck_command logging.info('Log output directory: %s', self.log_dir) logging.info('Mock app gen output directory: %s', self.output_dir) self.trace_cpu = config.trace_cpu self.switch_xcode_versions = config.switch_xcode_versions self.full_clean = config.full_clean self.run_xcodebuild = (not config.skip_xcode_build) self.test_build_only = config.test_build_only def main(self, args=sys.argv[1:]): logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(funcName)s: %(message)s') args = self.parse_config(args) self.config_to_vars(args) try: self.run_multisuite() except Exception as e: print(e) finally: self.multisuite_cleanup() logging.info("Done") # noinspection PyAttributeOutsideInit def make_context(self, log_dir, output_dir, test_build): self.cpu_logger = CPULogger() self.xcode_manager = XcodeManager() self.settings_state = SettingsState(output_dir) self.log_dir = log_dir self.output_dir = output_dir self.mock_output_dir = join(output_dir, 'apps', 'mockapp') self.mock_app_workspace = join(self.mock_output_dir, 'App', 'MockApp.xcworkspace') self.buckconfig_path = join(output_dir, '.buckconfig.local') self.sys_info_path = join(log_dir, 'system_info.txt') self.build_time_path = join(log_dir, 'build_times.txt') self.build_time_csv_path = join(log_dir, 'build_times.csv') self.build_trace_path = join(log_dir, 'build_traces') self.buck_path = '/apps/mockapp' self.app_buck_path = "/{}/App:MockApp".format(self.buck_path) has_dot = self.app_gen_options.dot_file_path and self.app_gen_options.dot_root_node_name if test_build: logging.info("Using test build settings") self.app_gen_options.lines_of_code = 100000 self.wmo_modes = [False] self.type_list = [ModuleGenType.dot] if has_dot else [ModuleGenType.flat] else: self.wmo_modes = [True, False] self.type_list = ModuleGenType.enum_list() if not has_dot: logging.warning("Removing dot mock app type due to lack of dot file to read. " "Specify one in the command line options.") self.type_list.remove(ModuleGenType.dot) def build_app_type(self, gen_type, wmo_enabled): xcode_version, xcode_build_id = XcodeManager.get_current_xcode_version() xcode_name = '{}_'.format(xcode_version.replace('.', '_')) build_log_path = join(self.log_dir, '{}{}_mockapp_build_log.txt'.format(xcode_name, gen_type)) gen_info = '{} (wmo_enabled: {}, xcode_version: {} {})'.format(gen_type, wmo_enabled, xcode_version, xcode_build_id) logging.info('##### Generating %s', gen_info) self.project_generator.use_wmo = wmo_enabled commandlineutil.del_old_output_dir(self.mock_output_dir) logging.info('Generating mock app') app_node, node_list = commandlineutil.gen_graph(gen_type, self.app_gen_options) self.project_generator.gen_app(app_node, node_list, self.app_gen_options.lines_of_code) swift_loc = commandlineutil.count_loc(self.mock_output_dir) logging.info('App type "%s" generated %d loc', gen_type, swift_loc) # Build App total_time = 0 if self.run_xcodebuild: logging.info('Generate xcode workspace & clean') derived_data_path = join(tempfile.gettempdir(), 'ub_mockapp_derived_data') shutil.rmtree(derived_data_path, ignore_errors=True) makedir(derived_data_path) subprocess.check_call([self.buck_binary, 'project', self.app_buck_path, '-d']) if self.full_clean: self.xcode_manager.clean_caches() logging.info('Start xcodebuild') start = time.time() with open(build_log_path, 'w') as build_log_file: subprocess.check_call([ 'xcodebuild', 'build', '-scheme', 'MockApp', '-sdk', 'iphonesimulator', '-workspace', self.mock_app_workspace, '-derivedDataPath', derived_data_path ], stdout=build_log_file, stderr=build_log_file) end = time.time() total_time = int(end - start) else: logging.info('Skipping xcodebuild & buck project') # Log Results build_end = str(datetime.datetime.now()) log_statement = '{} w/ {} (loc: {}) modules took {} s\n'.format(gen_info, len(node_list), swift_loc, total_time) logging.info(log_statement) self.build_time_file.write(log_statement) self.build_time_file.flush() full_xcode_version = xcode_version + " " + xcode_build_id self.build_time_csv_file.write('{}, {}, {}, {}, {}, {}, {}\n'.format( build_end, gen_type, full_xcode_version, wmo_enabled, total_time, len(node_list), swift_loc)) self.build_time_csv_file.flush() def verify_dependencies(self): if not self.run_xcodebuild: return # We don't need these binaries if we are not going to use them. xcode = ['xcodebuild', 'xcode-select'] local = [self.buck_binary] missing = check_dependent_commands(xcode + local) if missing == xcode: logging.error("Xcode command line tools do not seem to be installed / are missing in your path.") elif missing == local: logging.error("Specified buck command not available. Did you install it in your path?") if missing: logging.error("Missing required binaries: %s", str(missing)) raise OSError("Missing required binaries: {}".format(missing)) def dump_system_info(self): logging.info('Recording device info') with open(self.sys_info_path, 'w') as info_file: info_file.write(grab_mac_marketing_name()) info_file.write('\n') info_file.flush() subprocess.check_call(['system_profiler', 'SPHardwareDataType', '-detailLevel', 'mini'], stdout=info_file) subprocess.check_call(['sw_vers'], stdout=info_file) # noinspection PyAttributeOutsideInit def multisuite_setup(self): self.make_context(self.log_dir, self.output_dir, self.test_build_only) self.settings_state.save_buckconfig_local() self.settings_state.save_xcode_select() if self.switch_xcode_versions: self.sudo_warning() self.xcode_paths = self.xcode_manager.discover_xcode_versions() self.xcode_versions = self.xcode_paths.keys() logging.info("Discovered xcode verions: %s", str(self.xcode_versions)) else: self.xcode_paths = {} self.xcode_versions = [None] for path in [self.log_dir, self.build_trace_path, self.output_dir]: makedir(path) logging.info('Starting build session') self.build_time_file = open(self.build_time_path, 'a') self.build_time_csv_file = open(self.build_time_csv_path, 'a') now = str(datetime.datetime.now()) self.build_time_file.write('Build session started at {}\n'.format(now)) self.build_time_file.flush() self.dump_system_info() self.project_generator = projectgen.BuckProjectGenerator(self.mock_output_dir, self.buck_path) self.verify_dependencies() def multisuite_cleanup(self): logging.info("Cleaning up multisuite build test") self.settings_state.restore_buckconfig_local() if self.switch_xcode_versions: self.settings_state.restore_xcode_select() if self.build_time_file: self.build_time_file.close() if self.build_time_csv_file: self.build_time_csv_file.close() if self.trace_cpu: self.cpu_logger.kill() @staticmethod def sudo_warning(): if not sudo_enabled(): logging.warning('I would suggest executing this in a bash script and adding a sudo keep alive so you') logging.warning('can run this fully unattended: https://gist.github.com/cowboy/3118588') logging.warning("If you don't, then half way through the build suite, it will stall asking for a password.") logging.warning("I don't know how to make xcode-select -s not require sudo unfortunately.") def switch_xcode_version(self, xcode_version): logging.warning('Switching to xcode version %s', str(xcode_version)) self.xcode_manager.switch_xcode_version(self.xcode_paths[xcode_version]) def run_multisuite(self): self.multisuite_setup() start_time = time.time() if self.trace_cpu: self.cpu_logger.start() commandlineutil.make_custom_buckconfig_local(self.buckconfig_path) for xcode_version in self.xcode_versions: if self.switch_xcode_versions: self.switch_xcode_version(xcode_version) for wmo_enabled in self.wmo_modes: logging.info('Swift WMO Enabled: {}'.format(wmo_enabled)) for gen_type in self.type_list: self.build_app_type(gen_type, wmo_enabled) if self.trace_cpu: self.cpu_logger.stop() commandlineutil.apply_cpu_to_traces(self.build_trace_path, self.cpu_logger, start_time) def main(): CommandLineMultisuite().main() if __name__ == '__main__': main()