Egor Pasko | 0462e852d | 2018-03-29 15:52:09 | [diff] [blame] | 1 | #!/usr/bin/env vpython |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 2 | # 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 | |
| 8 | This shows some statistics about two orderfiles, possibly extracted from an |
| 9 | updating commit made by the orderfile bot. |
| 10 | """ |
| 11 | |
Raul Tambre | 48f17662 | 2019-09-23 10:05:24 | [diff] [blame] | 12 | from __future__ import print_function |
| 13 | |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 14 | import argparse |
Benoit Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 15 | import collections |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 16 | import logging |
| 17 | import os |
| 18 | import subprocess |
| 19 | import sys |
| 20 | |
| 21 | |
| 22 | def 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 Pasko | 895266a | 2018-11-13 15:34:01 | [diff] [blame] | 37 | # 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 Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 61 | return symbols |
| 62 | |
| 63 | |
Benoit Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 64 | def 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 | |
| 76 | CompareResult = collections.namedtuple( |
| 77 | 'CompareResult', ('first_count', 'second_count', |
| 78 | 'new_count', 'removed_count', |
| 79 | 'average_fractional_distance')) |
| 80 | |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 81 | def 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 Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 87 | |
| 88 | Returns: |
| 89 | An instance of CompareResult. |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 90 | """ |
| 91 | first_symbols = ParseOrderfile(first_filename) |
| 92 | second_symbols = ParseOrderfile(second_filename) |
Raul Tambre | 48f17662 | 2019-09-23 10:05:24 | [diff] [blame] | 93 | print('Symbols count:\n\tfirst:\t%d\n\tsecond:\t%d' % (len(first_symbols), |
| 94 | len(second_symbols))) |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 95 | 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 Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 99 | 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 Tambre | 48f17662 | 2019-09-23 10:05:24 | [diff] [blame] | 108 | 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 Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 112 | return CompareResult(len(first_symbols), len(second_symbols), |
| 113 | len(new_symbols), len(removed_symbols), |
| 114 | average_fractional_distance) |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 115 | |
| 116 | |
| 117 | def 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 Pasko | a784aab | 2020-04-03 17:06:00 | [diff] [blame] | 124 | output = subprocess.check_output(['git', 'show', r'--format=%s', commit_hash], |
| 125 | cwd=clank_path) |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 126 | first_line = output.split('\n')[0] |
Egor Pasko | a784aab | 2020-04-03 17:06:00 | [diff] [blame] | 127 | # 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 Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 131 | |
| 132 | |
| 133 | def 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 | |
| 156 | def 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 | |
| 164 | def 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 | |
| 181 | def 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 Pasko | 895266a | 2018-11-13 15:34:01 | [diff] [blame] | 186 | parser.add_argument('--keep', default=False, action='store_true', |
| 187 | help='Keep the downloaded orderfiles') |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 188 | parser.add_argument('--from-commit', help='Analyze the difference in the ' |
| 189 | 'orderfile from an orderfile bot commit.') |
Benoit Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 190 | parser.add_argument('--csv-output', help='Appends the result to a CSV file.') |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 191 | return parser |
| 192 | |
| 193 | |
| 194 | def 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 Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 205 | 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 Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 210 | finally: |
Egor Pasko | 895266a | 2018-11-13 15:34:01 | [diff] [blame] | 211 | if not args.keep: |
| 212 | os.remove(first) |
| 213 | os.remove(second) |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 214 | else: |
| 215 | return False |
| 216 | return True |
| 217 | |
| 218 | |
| 219 | if __name__ == '__main__': |
| 220 | sys.exit(0 if main() else 1) |