blob: e3266f051caafdd1863a00e4fe2aeb5ae544f968 [file] [log] [blame]
#!python
# Copyright 2012 Google Inc. All Rights Reserved.
#
# 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.
"""A utility script to automate the process of instrumenting, profiling and
optimizing Chrome."""
import chrome_utils
import instrument
import logging
import optparse
import os
import os.path
import profile
import runner
import shutil
import sys
import tempfile
_LOGGER = logging.getLogger(__name__)
class OptimizationError(Exception):
"""Raised on any failures in the optimization process."""
pass
def _ProcessBBEntries(log_files, output_dir):
"""Summarize the basic-block entry counts in @p log_files to a JSON file in
@p output_dir.
The file path to the generated JSON file is returned.
"""
output_file = os.path.join(output_dir, 'bbentries.json')
cmd = [
runner._GetExePath('grinder.exe'), # pylint: disable=W0212
'--mode=bbentry',
'--output-file=%s' % output_file,
]
cmd.extend(log_files)
ret = chrome_utils.Subprocess(cmd)
if ret != 0:
raise OptimizationError('Failed to process basic-block entries.')
return output_file
def _InstrumentAndProfile(work_dir, mode, opts):
"""Generate an instrumented chrome directory for @p mode in @p workdir using
the given @p opts and profile it.
If the mode is 'bbentry' generate a summary JSON file.
Returns the list of trace or summary files.
"""
instrumented_dir = os.path.join(work_dir, mode, 'instrumented')
profile_data_dir = os.path.join(work_dir, mode, 'profile-data')
# Instrument the provided Chrome executables in input_dir, and store
# the profiled executables in instrumented_dir.
instrument.InstrumentChrome(opts.input_dir, instrumented_dir, mode, [])
# Then profile the instrumented executables in instrumented_dir.
trace_files = profile.ProfileChrome(instrumented_dir,
profile_data_dir,
opts.iterations,
opts.chrome_frame,
opts.startup_type,
opts.startup_urls)
# For bbentry mode we need to run the grinder to generate a summary file
# to return.
if mode == 'bbentry':
summary_file = _ProcessBBEntries(trace_files, work_dir)
return [summary_file]
# Otherwise we just return the raw set of trace files.
return trace_files
# Give us silent access to the internals of our runner.
# pylint: disable=W0212
def _OptimizeChromeBinary(chrome_dir, work_dir, output_dir, log_files,
bb_entry_file, basename, optional):
"""Optimizes a single Chrome binary with the given |basename|. The original
binary in |chrome_dir| will be optimized using the data in the provided
|log_files|, assuming that they have been generated with respect to the
instrumented binary in |work_dir|. If |bb_entry_file| is provided then basic
block optimizations will also be applied.
Args:
chrome_dir: The directory holding the original Chrome binaries.
work_dir: The work directory containing the instrumented files.
output_dir: The directory where the optimized binaries will be written.
log_files: The calltrace log files to be used in generating an order file.
bb_entry_file: The BB entry counts to be used in optimizing the binary.
basename: The basename of the binary to optimize.
optional: If False then the binary must be present and must optimized.
Otherwise, this will do nothing and silently return if the input
binary does not exist.
"""
# Calculate all the path parameters.
input_image = os.path.join(chrome_dir, basename)
if not os.path.isfile(input_image):
if not optional:
raise OptimizationError('Missing mandatory "%s".' % basename)
_LOGGER.info('Not instrumenting missing optional "%s".', basename)
return
instrumented_image = os.path.join(
work_dir, 'calltrace', 'instrumented', basename)
order_file = os.path.join(work_dir, basename + '-order.json')
output_image = os.path.join(output_dir, basename)
# Generate the ordering file for the binary.
_LOGGER.info('Generating ordering file for "%s".', basename)
cmd = [
runner._GetExePath('reorder.exe'),
'--verbose',
'--output-stats',
'--input-image=%s' % input_image,
'--instrumented-image=%s' % instrumented_image,
'--output-file=%s' % order_file,
]
if bb_entry_file:
cmd.append('--basic_block_entry_counts=%s' % bb_entry_file)
cmd.extend(log_files)
ret = chrome_utils.Subprocess(cmd)
if ret != 0:
raise OptimizationError(
'Failed to generate an ordering for "%s".' % basename)
# Populate output_dir with a copy of the original chrome installation.
_LOGGER.info('Copying "%s" to output dir "%s".', chrome_dir, output_dir)
if os.path.isfile(output_dir):
raise OptimizationError('File present at output dir location: "%s"',
output_dir)
chrome_utils.CopyChromeFiles(chrome_dir, output_dir)
# Replace the binary in output_dir with an optimized version.
_LOGGER.info('Optimizing "%s".', basename)
cmd = [runner._GetExePath('relink.exe'),
'--verbose',
'--input-image=%s' % input_image,
'--output-image=%s' % output_image,
'--order-file=%s' % order_file,
'--overwrite']
ret = chrome_utils.Subprocess(cmd)
if ret != 0:
raise OptimizationError('Failed to relink "%s".' % basename)
def _OptimizeChrome(chrome_dir, work_dir, output_dir, log_files, bb_entry_file):
"""Generate an optimized version of the Chrome binaries in |chrome_dir| based
on the calltrace instrumented version in |work_dir|, the calltrace
|log_files| and an optional JSON |bb_entry_file|. The optimized Chrome
binaries will be written to |output_dir|.
chrome_dir: The directory holding the original Chrome binaries.
work_dir: The work directory containing the instrumented files.
output_dir: The directory where the optimized binaries will be written.
log_files: The calltrace log files to be used in generating an order file.
bb_entry_file: The BB entry counts to be used in optimizing the binary.
"""
for basename in instrument.EXECUTABLES:
_OptimizeChromeBinary(chrome_dir, work_dir, output_dir, log_files,
bb_entry_file, basename, False)
for basename in instrument.EXECUTABLES_OPTIONAL:
_OptimizeChromeBinary(chrome_dir, work_dir, output_dir, log_files,
bb_entry_file, basename, True)
def _CopyBinaries(orig_dir, src_dir, tgt_dir):
"""Copies the optimized Chrome binaries and PDB files from |src_dir| to
|tgt_dir|. For optional binaries it is expected that the copy succeed if a
copy of the binary also exists in |orig_dir|.
Args:
orig_dir: The directory containing the original binaries.
src_dir: The source directory.
tgt_dir: The target directory.
"""
if not os.path.isdir(tgt_dir):
_LOGGER.info('_CopyBinaries target dir not found. Creating "%s".', tgt_dir)
os.makedirs(tgt_dir)
# Generate a list of files to copy.
files = []
for path in instrument.EXECUTABLES:
files.append(path)
files.append(path + '.pdb')
for path in instrument.EXECUTABLES_OPTIONAL:
orig_file = os.path.join(orig_dir, path)
if os.path.isfile(orig_file):
files.append(path)
files.append(path + '.pdb')
# Copy over the files.
for path in files:
src_file = os.path.join(src_dir, path)
tgt_file = os.path.join(tgt_dir, path)
_LOGGER.info('Placing optimized "%s" in "%s".', path, tgt_dir)
shutil.copy2(src_file, tgt_file)
_USAGE = """%prog [options]
Instruments, then profiles the Chrome executables supplied in an input directory
for a number of iterations, then optimizes the executable order with respect to
the profile runs.
"""
def _ParseArguments():
"""Parse the sys.argv command-line arguments, returning the options."""
parser = optparse.OptionParser(usage=_USAGE)
parser.add_option('--verbose', dest='verbose',
default=False, action='store_true',
help='Verbose logging.')
parser.add_option('--iterations', dest='iterations', type='int',
default=10,
help='Number of profile iterations, 10 by default.')
parser.add_option('--chrome-frame', dest='chrome_frame',
default=False, action='store_true',
help=('Optimize for both Chrome Frame and Chrome usage '
'patterns. Without this flag, optimize only for '
'Chrome usage patterns.'))
parser.add_option('--input-dir', dest='input_dir',
help=('The input directory where the original Chrome '
'executables are to be found.'))
parser.add_option('--output-dir', dest='output_dir',
help=('The directory where the optimized chrome '
'installation will be created. From this location, '
'one can subsequently run benchmarks.'))
parser.add_option('--copy-to', dest='copy_to',
help=('(Optional) The output directory where the final '
'optimized PE and PDB files will be copied.'))
parser.add_option('--keep-temp-dirs', dest='keep_temp_dirs',
action='store_true',
help='Keep temp directories instead of deleting them.')
parser.add_option('--mode',
choices=instrument.MODES,
default=instrument.DEFAULT_MODE,
help='The instrumentation mode. Allowed values are: '
'%s (default: %%default).' % (
', '.join(instrument.MODES)))
parser.add_option('--startup-type', dest='startup_type', metavar='TYPE',
choices=runner.ALL_STARTUP_TYPES,
default=runner.DEFAULT_STARTUP_TYPE,
help='The type of Chrome session to open on startup. The '
'allowed values are: %s (default: %%default).' % (
', '.join(runner.ALL_STARTUP_TYPES)))
parser.add_option('--startup-url', dest='startup_urls', metavar='URL',
default=[], action='append',
help='Add URL to the startup scenario used for profiling. '
'This option may be given multiple times; each URL '
'will be added to the startup scenario.')
(opts, args) = parser.parse_args()
if len(args):
parser.error('Unexpected argument(s).')
# Minimally configure logging.
if opts.verbose:
logging.basicConfig(level=logging.INFO)
else:
logging.basicConfig(level=logging.WARNING)
if not opts.input_dir or not opts.output_dir:
parser.error('You must provide input and output directories')
opts.input_dir = os.path.abspath(opts.input_dir)
opts.output_dir = os.path.abspath(opts.output_dir)
if opts.copy_to:
opts.copy_to = os.path.abspath(opts.copy_to)
return opts
def main():
"""Parses arguments and runs the optimization."""
opts = _ParseArguments()
work_dir = tempfile.mkdtemp(prefix='chrome-instr')
_LOGGER.info('Created working directory "%s".', work_dir)
try:
# We always instrument in calltrace mode to be able to generate a coarse
# function layout for the binary.
trace_files = _InstrumentAndProfile(work_dir, 'calltrace', opts)
# If we're in bbentry mode then we further instrument and profile to get
# summary stats for basic-block entry counts.
if opts.mode == 'bbentry':
bb_entry_file = _InstrumentAndProfile(work_dir, 'bbentry', opts)[0]
else:
bb_entry_file = None
# Lastly generate an ordering, and reorder the inputs to
# the output dir.
_OptimizeChrome(
opts.input_dir, work_dir, opts.output_dir, trace_files, bb_entry_file)
if opts.copy_to:
_CopyBinaries(opts.input_dir, opts.output_dir, opts.copy_to)
except OptimizationError:
_LOGGER.exception('Optimization failed.')
return 1
finally:
if opts.keep_temp_dirs:
_LOGGER.info('Keeping temporary directory "%s".', work_dir)
else:
_LOGGER.info('Deleting temporary directory "%s".', work_dir)
chrome_utils.RmTree(work_dir)
return 0
if __name__ == '__main__':
sys.exit(main())