#!/usr/bin/env vpython3
# Copyright 2017 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Compares two orderfiles, from filenames or a commit.
This shows some statistics about two orderfiles, possibly extracted from an
updating commit made by the orderfile bot.
"""
from __future__ import print_function
import argparse
import collections
import logging
import os
import subprocess
import sys
def ParseOrderfile(filename):
"""Parses an orderfile into a list of symbols.
Args:
filename: (str) Path to the orderfile.
Returns:
[str] List of symbols.
"""
symbols = []
lines = []
already_seen = set()
with open(filename, 'r') as f:
lines = [line.strip() for line in f]
# The (new) orderfiles that are oriented at the LLD linker contain only symbol
# names (i.e. not prefixed with '.text'). The (old) orderfiles aimed at the
# Gold linker were patched by duplicating symbols prefixed with '.text.hot.',
# '.text.unlikely.' and '.text.', hence the appearance of '.text' on the first
# symbol indicates such a legacy orderfile.
if not lines[0].startswith('.text.'):
for entry in lines:
symbol_name = entry.rstrip('\n')
assert symbol_name not in ('*', '.text')
already_seen.add(symbol_name)
symbols.append(symbol_name)
else:
for entry in lines:
# Keep only (input) section names, not symbol names (only rare special
# symbols contain '.'). We could only keep symbols, but then some even
# older orderfiles would not be parsed.
if '.' not in entry:
continue
# Example: .text.startup.BLA
symbol_name = entry[entry.rindex('.'):]
if symbol_name in already_seen or symbol_name == '*' or entry == '.text':
continue
already_seen.add(symbol_name)
symbols.append(symbol_name)
return symbols
def CommonSymbolsToOrder(symbols, common_symbols):
"""Returns s -> index for all s in common_symbols."""
result = {}
index = 0
for s in symbols:
if s not in common_symbols:
continue
result[s] = index
index += 1
return result
CompareResult = collections.namedtuple(
'CompareResult', ('first_count', 'second_count',
'new_count', 'removed_count',
'average_fractional_distance'))
def Compare(first_filename, second_filename):
"""Outputs a comparison of two orderfiles to stdout.
Args:
first_filename: (str) First orderfile.
second_filename: (str) Second orderfile.
Returns:
An instance of CompareResult.
"""
first_symbols = ParseOrderfile(first_filename)
second_symbols = ParseOrderfile(second_filename)
print('Symbols count:\n\tfirst:\t%d\n\tsecond:\t%d' % (len(first_symbols),
len(second_symbols)))
first_symbols = set(first_symbols)
second_symbols = set(second_symbols)
new_symbols = second_symbols - first_symbols
removed_symbols = first_symbols - second_symbols
common_symbols = first_symbols & second_symbols
# Distance between orderfiles.
first_to_ordering = CommonSymbolsToOrder(first_symbols, common_symbols)
second_to_ordering = CommonSymbolsToOrder(second_symbols, common_symbols)
total_distance = sum(abs(first_to_ordering[s] - second_to_ordering[s])\
for s in first_to_ordering)
# Each distance is in [0, len(common_symbols)] and there are
# len(common_symbols) entries, hence the normalization.
average_fractional_distance = float(total_distance) / (len(common_symbols)**2)
print('New symbols = %d' % len(new_symbols))
print('Removed symbols = %d' % len(removed_symbols))
print('Average fractional distance = %.2f%%' %
(100. * average_fractional_distance))
return CompareResult(len(first_symbols), len(second_symbols),
len(new_symbols), len(removed_symbols),
average_fractional_distance)
def CheckOrderfileCommit(commit_hash, clank_path):
"""Asserts that a commit is an orderfile update from the bot.
Args:
commit_hash: (str) Git hash of the orderfile roll commit.
clank_path: (str) Path to the clank repository.
"""
output = subprocess.check_output(['git', 'show', r'--format=%s', commit_hash],
cwd=clank_path)
first_line = output.decode().split('\n')[0]
# Capitalization changed at some point. Not checking the bot name because it
# changed too.
assert first_line.upper().endswith(
'Update Orderfile.'.upper()), ('Not an orderfile commit')
def GetBeforeAfterOrderfileHashes(commit_hash, clank_path):
"""Downloads the orderfiles before and afer an orderfile roll.
Args:
commit_hash: (str) Git hash of the orderfile roll commit.
clank_path: (str) Path to the clank repository.
Returns:
(str, str) Path to the before and after commit orderfiles.
"""
orderfile_hash_relative_path = 'orderfiles/orderfile.arm.out.sha1'
before_output = subprocess.check_output(
['git', 'show', '%s^:%s' % (commit_hash, orderfile_hash_relative_path)],
cwd=clank_path)
before_hash = before_output.decode().split('\n')[0]
after_output = subprocess.check_output(
['git', 'show', '%s:%s' % (commit_hash, orderfile_hash_relative_path)],
cwd=clank_path)
after_hash = after_output.decode().split('\n')[0]
assert before_hash != after_hash
return (before_hash, after_hash)
def DownloadOrderfile(orderfile_hash, output_filename):
"""Downloads an orderfile with a given hash to a given destination."""
cloud_storage_path = (
'gs://clank-archive/orderfile-clankium/%s' % orderfile_hash)
subprocess.check_call(
['gsutil.py', 'cp', cloud_storage_path, output_filename])
def GetOrderfilesFromCommit(commit_hash):
"""Returns paths to the before and after orderfiles for a commit."""
clank_path = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir,
'clank')
logging.info('Checking that the commit is an orderfile')
CheckOrderfileCommit(commit_hash, clank_path)
(before_hash, after_hash) = GetBeforeAfterOrderfileHashes(
commit_hash, clank_path)
logging.info('Before / after hashes: %s %s', before_hash, after_hash)
before_filename = os.path.join('/tmp/', before_hash)
after_filename = os.path.join('/tmp/', after_hash)
logging.info('Downloading files')
DownloadOrderfile(before_hash, before_filename)
DownloadOrderfile(after_hash, after_filename)
return (before_filename, after_filename)
def CreateArgumentParser():
"""Returns the argumeng parser."""
parser = argparse.ArgumentParser()
parser.add_argument('--first', help='First orderfile')
parser.add_argument('--second', help='Second orderfile')
parser.add_argument('--keep', default=False, action='store_true',
help='Keep the downloaded orderfiles')
parser.add_argument('--from-commit', help='Analyze the difference in the '
'orderfile from an orderfile bot commit.')
parser.add_argument('--csv-output', help='Appends the result to a CSV file.')
return parser
def main():
logging.basicConfig(level=logging.INFO)
parser = CreateArgumentParser()
args = parser.parse_args()
if args.first or args.second:
assert args.first and args.second, 'Need both files.'
Compare(args.first, args.second)
elif args.from_commit:
first, second = GetOrderfilesFromCommit(args.from_commit)
try:
logging.info('Comparing the orderfiles')
result = Compare(first, second)
if args.csv_output:
with open(args.csv_output, 'a') as f:
f.write('%s,%d,%d,%d,%d,%f\n' % tuple(
[args.from_commit] + list(result)))
finally:
if not args.keep:
os.remove(first)
os.remove(second)
else:
return False
return True
if __name__ == '__main__':
sys.exit(0 if main() else 1)