blob: 272afeb04176748663f692afa90286e1435a14e6 [file] [log] [blame]
Egor Pasko0462e852d2018-03-29 15:52:091#!/usr/bin/env vpython
Benoit Lize96236662017-12-08 17:49:142# Copyright 2017 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
6"""Compares two orderfiles, from filenames or a commit.
7
8This shows some statistics about two orderfiles, possibly extracted from an
9updating commit made by the orderfile bot.
10"""
11
12import argparse
Benoit Lizec6569bb2018-02-28 14:02:5413import collections
Benoit Lize96236662017-12-08 17:49:1414import logging
15import os
16import subprocess
17import sys
18
19
20def ParseOrderfile(filename):
21 """Parses an orderfile into a list of symbols.
22
23 Args:
24 filename: (str) Path to the orderfile.
25
26 Returns:
27 [str] List of symbols.
28 """
29 symbols = []
30 lines = []
31 already_seen = set()
32 with open(filename, 'r') as f:
33 lines = [line.strip() for line in f]
34
35 for entry in lines:
Benoit Lizec6569bb2018-02-28 14:02:5436 # Keep only sections, not symbols (symbols don't contain '.').
37 # We could only keep symbols, but then old orderfiles would not be parsed.
38 if '.' not in entry:
39 continue
Benoit Lize96236662017-12-08 17:49:1440 # Example: .text.startup.BLA
41 symbol_name = entry[entry.rindex('.'):]
42 if symbol_name in already_seen or symbol_name == '*' or entry == '.text':
43 continue
44 already_seen.add(symbol_name)
45 symbols.append(symbol_name)
46 return symbols
47
48
Benoit Lizec6569bb2018-02-28 14:02:5449def CommonSymbolsToOrder(symbols, common_symbols):
50 """Returns s -> index for all s in common_symbols."""
51 result = {}
52 index = 0
53 for s in symbols:
54 if s not in common_symbols:
55 continue
56 result[s] = index
57 index += 1
58 return result
59
60
61CompareResult = collections.namedtuple(
62 'CompareResult', ('first_count', 'second_count',
63 'new_count', 'removed_count',
64 'average_fractional_distance'))
65
Benoit Lize96236662017-12-08 17:49:1466def Compare(first_filename, second_filename):
67 """Outputs a comparison of two orderfiles to stdout.
68
69 Args:
70 first_filename: (str) First orderfile.
71 second_filename: (str) Second orderfile.
Benoit Lizec6569bb2018-02-28 14:02:5472
73 Returns:
74 An instance of CompareResult.
Benoit Lize96236662017-12-08 17:49:1475 """
76 first_symbols = ParseOrderfile(first_filename)
77 second_symbols = ParseOrderfile(second_filename)
78 print 'Symbols count:\n\tfirst:\t%d\n\tsecond:\t%d' % (
79 len(first_symbols), len(second_symbols))
80 first_symbols = set(first_symbols)
81 second_symbols = set(second_symbols)
82 new_symbols = second_symbols - first_symbols
83 removed_symbols = first_symbols - second_symbols
Benoit Lizec6569bb2018-02-28 14:02:5484 common_symbols = first_symbols & second_symbols
85 # Distance between orderfiles.
86 first_to_ordering = CommonSymbolsToOrder(first_symbols, common_symbols)
87 second_to_ordering = CommonSymbolsToOrder(second_symbols, common_symbols)
88 total_distance = sum(abs(first_to_ordering[s] - second_to_ordering[s])\
89 for s in first_to_ordering)
90 # Each distance is in [0, len(common_symbols)] and there are
91 # len(common_symbols) entries, hence the normalization.
92 average_fractional_distance = float(total_distance) / (len(common_symbols)**2)
Benoit Lize96236662017-12-08 17:49:1493 print 'New symbols = %d' % len(new_symbols)
94 print 'Removed symbols = %d' % len(removed_symbols)
Benoit Lizec6569bb2018-02-28 14:02:5495 print 'Average fractional distance = %.2f%%' % (
96 100. * average_fractional_distance)
97 return CompareResult(len(first_symbols), len(second_symbols),
98 len(new_symbols), len(removed_symbols),
99 average_fractional_distance)
Benoit Lize96236662017-12-08 17:49:14100
101
102def CheckOrderfileCommit(commit_hash, clank_path):
103 """Asserts that a commit is an orderfile update from the bot.
104
105 Args:
106 commit_hash: (str) Git hash of the orderfile roll commit.
107 clank_path: (str) Path to the clank repository.
108 """
109 output = subprocess.check_output(
110 ['git', 'show', r'--format=%an %s', commit_hash], cwd=clank_path)
111 first_line = output.split('\n')[0]
Benoit Lizec6569bb2018-02-28 14:02:54112 # Capitalization changed at some point.
113 assert first_line.upper() == 'clank-autoroller Update Orderfile.'.upper(), (
Benoit Lize96236662017-12-08 17:49:14114 'Not an orderfile commit')
115
116
117def GetBeforeAfterOrderfileHashes(commit_hash, clank_path):
118 """Downloads the orderfiles before and afer an orderfile roll.
119
120 Args:
121 commit_hash: (str) Git hash of the orderfile roll commit.
122 clank_path: (str) Path to the clank repository.
123
124 Returns:
125 (str, str) Path to the before and after commit orderfiles.
126 """
127 orderfile_hash_relative_path = 'orderfiles/orderfile.arm.out.sha1'
128 before_output = subprocess.check_output(
129 ['git', 'show', '%s^:%s' % (commit_hash, orderfile_hash_relative_path)],
130 cwd=clank_path)
131 before_hash = before_output.split('\n')[0]
132 after_output = subprocess.check_output(
133 ['git', 'show', '%s:%s' % (commit_hash, orderfile_hash_relative_path)],
134 cwd=clank_path)
135 after_hash = after_output.split('\n')[0]
136 assert before_hash != after_hash
137 return (before_hash, after_hash)
138
139
140def DownloadOrderfile(orderfile_hash, output_filename):
141 """Downloads an orderfile with a given hash to a given destination."""
142 cloud_storage_path = (
143 'gs://clank-archive/orderfile-clankium/%s' % orderfile_hash)
144 subprocess.check_call(
145 ['gsutil.py', 'cp', cloud_storage_path, output_filename])
146
147
148def GetOrderfilesFromCommit(commit_hash):
149 """Returns paths to the before and after orderfiles for a commit."""
150 clank_path = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir,
151 'clank')
152 logging.info('Checking that the commit is an orderfile')
153 CheckOrderfileCommit(commit_hash, clank_path)
154 (before_hash, after_hash) = GetBeforeAfterOrderfileHashes(
155 commit_hash, clank_path)
156 logging.info('Before / after hashes: %s %s', before_hash, after_hash)
157 before_filename = os.path.join('/tmp/', before_hash)
158 after_filename = os.path.join('/tmp/', after_hash)
159 logging.info('Downloading files')
160 DownloadOrderfile(before_hash, before_filename)
161 DownloadOrderfile(after_hash, after_filename)
162 return (before_filename, after_filename)
163
164
165def CreateArgumentParser():
166 """Returns the argumeng parser."""
167 parser = argparse.ArgumentParser()
168 parser.add_argument('--first', help='First orderfile')
169 parser.add_argument('--second', help='Second orderfile')
170 parser.add_argument('--from-commit', help='Analyze the difference in the '
171 'orderfile from an orderfile bot commit.')
Benoit Lizec6569bb2018-02-28 14:02:54172 parser.add_argument('--csv-output', help='Appends the result to a CSV file.')
Benoit Lize96236662017-12-08 17:49:14173 return parser
174
175
176def main():
177 logging.basicConfig(level=logging.INFO)
178 parser = CreateArgumentParser()
179 args = parser.parse_args()
180 if args.first or args.second:
181 assert args.first and args.second, 'Need both files.'
182 Compare(args.first, args.second)
183 elif args.from_commit:
184 first, second = GetOrderfilesFromCommit(args.from_commit)
185 try:
186 logging.info('Comparing the orderfiles')
Benoit Lizec6569bb2018-02-28 14:02:54187 result = Compare(first, second)
188 if args.csv_output:
189 with open(args.csv_output, 'a') as f:
190 f.write('%s,%d,%d,%d,%d,%f\n' % tuple(
191 [args.from_commit] + list(result)))
Benoit Lize96236662017-12-08 17:49:14192 finally:
193 os.remove(first)
194 os.remove(second)
195 else:
196 return False
197 return True
198
199
200if __name__ == '__main__':
201 sys.exit(0 if main() else 1)