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