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 | |
| 12 | import argparse |
Benoit Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 13 | import collections |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 14 | import logging |
| 15 | import os |
| 16 | import subprocess |
| 17 | import sys |
| 18 | |
| 19 | |
| 20 | def 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 Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 36 | # 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 Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 40 | # 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 Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 49 | def 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 | |
| 61 | CompareResult = collections.namedtuple( |
| 62 | 'CompareResult', ('first_count', 'second_count', |
| 63 | 'new_count', 'removed_count', |
| 64 | 'average_fractional_distance')) |
| 65 | |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 66 | def 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 Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 72 | |
| 73 | Returns: |
| 74 | An instance of CompareResult. |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 75 | """ |
| 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 Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 84 | 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 Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 93 | print 'New symbols = %d' % len(new_symbols) |
| 94 | print 'Removed symbols = %d' % len(removed_symbols) |
Benoit Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 95 | 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 Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 100 | |
| 101 | |
| 102 | def 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 Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 112 | # Capitalization changed at some point. |
| 113 | assert first_line.upper() == 'clank-autoroller Update Orderfile.'.upper(), ( |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 114 | 'Not an orderfile commit') |
| 115 | |
| 116 | |
| 117 | def 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 | |
| 140 | def 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 | |
| 148 | def 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 | |
| 165 | def 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 Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 172 | parser.add_argument('--csv-output', help='Appends the result to a CSV file.') |
Benoit Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 173 | return parser |
| 174 | |
| 175 | |
| 176 | def 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 Lize | c6569bb | 2018-02-28 14:02:54 | [diff] [blame] | 187 | 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 Lize | 9623666 | 2017-12-08 17:49:14 | [diff] [blame] | 192 | finally: |
| 193 | os.remove(first) |
| 194 | os.remove(second) |
| 195 | else: |
| 196 | return False |
| 197 | return True |
| 198 | |
| 199 | |
| 200 | if __name__ == '__main__': |
| 201 | sys.exit(0 if main() else 1) |