• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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