1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# Copyright 2020 The Chromium OS Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Script to remove cold functions in an textual AFDO profile. 8 9The script will look through the AFDO profile to find all the function 10records. Then it'll start with the functions with lowest sample count and 11remove it from the profile, until the total remaining functions in the 12profile meets the given number. When there are many functions having the 13same sample count, we need to remove all of them in order to meet the 14target, so the result profile will always have less than or equal to the 15given number of functions. 16 17The script is intended to be used on production Chrome OS profiles, after 18other redaction/trimming scripts. It can be used with given textual CWP 19and benchmark profiles, in order to analyze how many removed functions are 20from which profile (or both), which can be used an indicator of fairness 21during the removal. 22 23This is part of the effort to stablize the impact of AFDO profile on 24Chrome binary size. See crbug.com/1062014 for more context. 25""" 26 27from __future__ import division, print_function 28 29import argparse 30import collections 31import re 32import sys 33 34_function_line_re = re.compile(r'^([\w\$\.@]+):(\d+)(?::\d+)?$') 35ProfileRecord = collections.namedtuple( 36 'ProfileRecord', ['function_count', 'function_body', 'function_name']) 37 38 39def _read_sample_count(line): 40 m = _function_line_re.match(line) 41 assert m, 'Failed to interpret function line %s' % line 42 return m.group(1), int(m.group(2)) 43 44 45def _read_textual_afdo_profile(stream): 46 """Parses an AFDO profile from a line stream into ProfileRecords.""" 47 # ProfileRecords are actually nested, due to inlining. For the purpose of 48 # this script, that doesn't matter. 49 lines = (line.rstrip() for line in stream) 50 function_line = None 51 samples = [] 52 ret = [] 53 for line in lines: 54 if not line: 55 continue 56 57 if line[0].isspace(): 58 assert function_line is not None, 'sample exists outside of a function?' 59 samples.append(line) 60 continue 61 62 if function_line is not None: 63 name, count = _read_sample_count(function_line) 64 body = [function_line] + samples 65 ret.append( 66 ProfileRecord( 67 function_count=count, function_body=body, function_name=name)) 68 function_line = line 69 samples = [] 70 71 if function_line is not None: 72 name, count = _read_sample_count(function_line) 73 body = [function_line] + samples 74 ret.append( 75 ProfileRecord( 76 function_count=count, function_body=body, function_name=name)) 77 return ret 78 79 80def write_textual_afdo_profile(stream, records): 81 for r in records: 82 print('\n'.join(r.function_body), file=stream) 83 84 85def analyze_functions(records, cwp, benchmark): 86 cwp_functions = {x.function_name for x in cwp} 87 benchmark_functions = {x.function_name for x in benchmark} 88 all_functions = {x.function_name for x in records} 89 cwp_only_functions = len((all_functions & cwp_functions) - 90 benchmark_functions) 91 benchmark_only_functions = len((all_functions & benchmark_functions) - 92 cwp_functions) 93 common_functions = len(all_functions & benchmark_functions & cwp_functions) 94 none_functions = len(all_functions - benchmark_functions - cwp_functions) 95 96 assert not none_functions 97 return cwp_only_functions, benchmark_only_functions, common_functions 98 99 100def run(input_stream, output_stream, goal, cwp=None, benchmark=None): 101 records = _read_textual_afdo_profile(input_stream) 102 num_functions = len(records) 103 if not num_functions: 104 return 105 assert goal, "It's invalid to remove all functions in the profile" 106 107 if cwp and benchmark: 108 cwp_records = _read_textual_afdo_profile(cwp) 109 benchmark_records = _read_textual_afdo_profile(benchmark) 110 cwp_num, benchmark_num, common_num = analyze_functions( 111 records, cwp_records, benchmark_records) 112 113 records.sort(key=lambda x: (-x.function_count, x.function_name)) 114 records = records[:goal] 115 116 print( 117 'Retained %d/%d (%.1f%%) functions in the profile' % 118 (len(records), num_functions, 100.0 * len(records) / num_functions), 119 file=sys.stderr) 120 write_textual_afdo_profile(output_stream, records) 121 122 if cwp and benchmark: 123 cwp_num_after, benchmark_num_after, common_num_after = analyze_functions( 124 records, cwp_records, benchmark_records) 125 print( 126 'Retained %d/%d (%.1f%%) functions only appear in the CWP profile' % 127 (cwp_num_after, cwp_num, 100.0 * cwp_num_after / cwp_num), 128 file=sys.stderr) 129 print( 130 'Retained %d/%d (%.1f%%) functions only appear in the benchmark profile' 131 % (benchmark_num_after, benchmark_num, 132 100.0 * benchmark_num_after / benchmark_num), 133 file=sys.stderr) 134 print( 135 'Retained %d/%d (%.1f%%) functions appear in both CWP and benchmark' 136 ' profiles' % (common_num_after, common_num, 137 100.0 * common_num_after / common_num), 138 file=sys.stderr) 139 140 141def main(): 142 parser = argparse.ArgumentParser( 143 description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) 144 parser.add_argument( 145 '--input', 146 default='/dev/stdin', 147 help='File to read from. Defaults to stdin.') 148 parser.add_argument( 149 '--output', 150 default='/dev/stdout', 151 help='File to write to. Defaults to stdout.') 152 parser.add_argument( 153 '--number', 154 type=int, 155 required=True, 156 help='Number of functions to retain in the profile.') 157 parser.add_argument( 158 '--cwp', help='Textualized CWP profiles, used for further analysis') 159 parser.add_argument( 160 '--benchmark', 161 help='Textualized benchmark profile, used for further analysis') 162 args = parser.parse_args() 163 164 if not args.number: 165 parser.error("It's invalid to remove the number of functions to 0.") 166 167 if (args.cwp and not args.benchmark) or (not args.cwp and args.benchmark): 168 parser.error('Please specify both --cwp and --benchmark') 169 170 with open(args.input) as stdin: 171 with open(args.output, 'w') as stdout: 172 # When user specify textualized cwp and benchmark profiles, perform 173 # the analysis. Otherwise, just trim the cold functions from profile. 174 if args.cwp and args.benchmark: 175 with open(args.cwp) as cwp: 176 with open(args.benchmark) as benchmark: 177 run(stdin, stdout, args.number, cwp, benchmark) 178 else: 179 run(stdin, stdout, args.number) 180 181 182if __name__ == '__main__': 183 main() 184