xref: /aosp_15_r20/external/toolchain-utils/afdo_redaction/remove_cold_functions.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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