blob: 6e48a80852f26f4ba7c1985e47e7b39696a3d932 [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
Raul Tambre48f176622019-09-23 10:05:2412from __future__ import print_function
13
Benoit Lize96236662017-12-08 17:49:1414import argparse
Benoit Lizec6569bb2018-02-28 14:02:5415import collections
Benoit Lize96236662017-12-08 17:49:1416import logging
17import os
18import subprocess
19import sys
20
21
22def ParseOrderfile(filename):
23 """Parses an orderfile into a list of symbols.
24
25 Args:
26 filename: (str) Path to the orderfile.
27
28 Returns:
29 [str] List of symbols.
30 """
31 symbols = []
32 lines = []
33 already_seen = set()
34 with open(filename, 'r') as f:
35 lines = [line.strip() for line in f]
36
Egor Pasko895266a2018-11-13 15:34:0137 # The (new) orderfiles that are oriented at the LLD linker contain only symbol
38 # names (i.e. not prefixed with '.text'). The (old) orderfiles aimed at the
39 # Gold linker were patched by duplicating symbols prefixed with '.text.hot.',
40 # '.text.unlikely.' and '.text.', hence the appearance of '.text' on the first
41 # symbol indicates such a legacy orderfile.
42 if not lines[0].startswith('.text.'):
43 for entry in lines:
44 symbol_name = entry.rstrip('\n')
45 assert symbol_name != '*' and symbol_name != '.text'
46 already_seen.add(symbol_name)
47 symbols.append(symbol_name)
48 else:
49 for entry in lines:
50 # Keep only (input) section names, not symbol names (only rare special
51 # symbols contain '.'). We could only keep symbols, but then some even
52 # older orderfiles would not be parsed.
53 if '.' not in entry:
54 continue
55 # Example: .text.startup.BLA
56 symbol_name = entry[entry.rindex('.'):]
57 if symbol_name in already_seen or symbol_name == '*' or entry == '.text':
58 continue
59 already_seen.add(symbol_name)
60 symbols.append(symbol_name)
Benoit Lize96236662017-12-08 17:49:1461 return symbols
62
63
Benoit Lizec6569bb2018-02-28 14:02:5464def CommonSymbolsToOrder(symbols, common_symbols):
65 """Returns s -> index for all s in common_symbols."""
66 result = {}
67 index = 0
68 for s in symbols:
69 if s not in common_symbols:
70 continue
71 result[s] = index
72 index += 1
73 return result
74
75
76CompareResult = collections.namedtuple(
77 'CompareResult', ('first_count', 'second_count',
78 'new_count', 'removed_count',
79 'average_fractional_distance'))
80
Benoit Lize96236662017-12-08 17:49:1481def Compare(first_filename, second_filename):
82 """Outputs a comparison of two orderfiles to stdout.
83
84 Args:
85 first_filename: (str) First orderfile.
86 second_filename: (str) Second orderfile.
Benoit Lizec6569bb2018-02-28 14:02:5487
88 Returns:
89 An instance of CompareResult.
Benoit Lize96236662017-12-08 17:49:1490 """
91 first_symbols = ParseOrderfile(first_filename)
92 second_symbols = ParseOrderfile(second_filename)
Raul Tambre48f176622019-09-23 10:05:2493 print('Symbols count:\n\tfirst:\t%d\n\tsecond:\t%d' % (len(first_symbols),
94 len(second_symbols)))
Benoit Lize96236662017-12-08 17:49:1495 first_symbols = set(first_symbols)
96 second_symbols = set(second_symbols)
97 new_symbols = second_symbols - first_symbols
98 removed_symbols = first_symbols - second_symbols
Benoit Lizec6569bb2018-02-28 14:02:5499 common_symbols = first_symbols & second_symbols
100 # Distance between orderfiles.
101 first_to_ordering = CommonSymbolsToOrder(first_symbols, common_symbols)
102 second_to_ordering = CommonSymbolsToOrder(second_symbols, common_symbols)
103 total_distance = sum(abs(first_to_ordering[s] - second_to_ordering[s])\
104 for s in first_to_ordering)
105 # Each distance is in [0, len(common_symbols)] and there are
106 # len(common_symbols) entries, hence the normalization.
107 average_fractional_distance = float(total_distance) / (len(common_symbols)**2)
Raul Tambre48f176622019-09-23 10:05:24108 print('New symbols = %d' % len(new_symbols))
109 print('Removed symbols = %d' % len(removed_symbols))
110 print('Average fractional distance = %.2f%%' %
111 (100. * average_fractional_distance))
Benoit Lizec6569bb2018-02-28 14:02:54112 return CompareResult(len(first_symbols), len(second_symbols),
113 len(new_symbols), len(removed_symbols),
114 average_fractional_distance)
Benoit Lize96236662017-12-08 17:49:14115
116
117def CheckOrderfileCommit(commit_hash, clank_path):
118 """Asserts that a commit is an orderfile update from the bot.
119
120 Args:
121 commit_hash: (str) Git hash of the orderfile roll commit.
122 clank_path: (str) Path to the clank repository.
123 """
Egor Paskoa784aab2020-04-03 17:06:00124 output = subprocess.check_output(['git', 'show', r'--format=%s', commit_hash],
125 cwd=clank_path)
Benoit Lize96236662017-12-08 17:49:14126 first_line = output.split('\n')[0]
Egor Paskoa784aab2020-04-03 17:06:00127 # Capitalization changed at some point. Not checking the bot name because it
128 # changed too.
129 assert first_line.upper().endswith(
130 'Update Orderfile.'.upper()), ('Not an orderfile commit')
Benoit Lize96236662017-12-08 17:49:14131
132
133def GetBeforeAfterOrderfileHashes(commit_hash, clank_path):
134 """Downloads the orderfiles before and afer an orderfile roll.
135
136 Args:
137 commit_hash: (str) Git hash of the orderfile roll commit.
138 clank_path: (str) Path to the clank repository.
139
140 Returns:
141 (str, str) Path to the before and after commit orderfiles.
142 """
143 orderfile_hash_relative_path = 'orderfiles/orderfile.arm.out.sha1'
144 before_output = subprocess.check_output(
145 ['git', 'show', '%s^:%s' % (commit_hash, orderfile_hash_relative_path)],
146 cwd=clank_path)
147 before_hash = before_output.split('\n')[0]
148 after_output = subprocess.check_output(
149 ['git', 'show', '%s:%s' % (commit_hash, orderfile_hash_relative_path)],
150 cwd=clank_path)
151 after_hash = after_output.split('\n')[0]
152 assert before_hash != after_hash
153 return (before_hash, after_hash)
154
155
156def DownloadOrderfile(orderfile_hash, output_filename):
157 """Downloads an orderfile with a given hash to a given destination."""
158 cloud_storage_path = (
159 'gs://clank-archive/orderfile-clankium/%s' % orderfile_hash)
160 subprocess.check_call(
161 ['gsutil.py', 'cp', cloud_storage_path, output_filename])
162
163
164def GetOrderfilesFromCommit(commit_hash):
165 """Returns paths to the before and after orderfiles for a commit."""
166 clank_path = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir,
167 'clank')
168 logging.info('Checking that the commit is an orderfile')
169 CheckOrderfileCommit(commit_hash, clank_path)
170 (before_hash, after_hash) = GetBeforeAfterOrderfileHashes(
171 commit_hash, clank_path)
172 logging.info('Before / after hashes: %s %s', before_hash, after_hash)
173 before_filename = os.path.join('/tmp/', before_hash)
174 after_filename = os.path.join('/tmp/', after_hash)
175 logging.info('Downloading files')
176 DownloadOrderfile(before_hash, before_filename)
177 DownloadOrderfile(after_hash, after_filename)
178 return (before_filename, after_filename)
179
180
181def CreateArgumentParser():
182 """Returns the argumeng parser."""
183 parser = argparse.ArgumentParser()
184 parser.add_argument('--first', help='First orderfile')
185 parser.add_argument('--second', help='Second orderfile')
Egor Pasko895266a2018-11-13 15:34:01186 parser.add_argument('--keep', default=False, action='store_true',
187 help='Keep the downloaded orderfiles')
Benoit Lize96236662017-12-08 17:49:14188 parser.add_argument('--from-commit', help='Analyze the difference in the '
189 'orderfile from an orderfile bot commit.')
Benoit Lizec6569bb2018-02-28 14:02:54190 parser.add_argument('--csv-output', help='Appends the result to a CSV file.')
Benoit Lize96236662017-12-08 17:49:14191 return parser
192
193
194def main():
195 logging.basicConfig(level=logging.INFO)
196 parser = CreateArgumentParser()
197 args = parser.parse_args()
198 if args.first or args.second:
199 assert args.first and args.second, 'Need both files.'
200 Compare(args.first, args.second)
201 elif args.from_commit:
202 first, second = GetOrderfilesFromCommit(args.from_commit)
203 try:
204 logging.info('Comparing the orderfiles')
Benoit Lizec6569bb2018-02-28 14:02:54205 result = Compare(first, second)
206 if args.csv_output:
207 with open(args.csv_output, 'a') as f:
208 f.write('%s,%d,%d,%d,%d,%f\n' % tuple(
209 [args.from_commit] + list(result)))
Benoit Lize96236662017-12-08 17:49:14210 finally:
Egor Pasko895266a2018-11-13 15:34:01211 if not args.keep:
212 os.remove(first)
213 os.remove(second)
Benoit Lize96236662017-12-08 17:49:14214 else:
215 return False
216 return True
217
218
219if __name__ == '__main__':
220 sys.exit(0 if main() else 1)