xref: /aosp_15_r20/art/tools/bisect_profile.py (revision 795d594fd825385562da6b089ea9b2033f3abf5a)
1*795d594fSAndroid Build Coastguard Worker#!/usr/bin/python3
2*795d594fSAndroid Build Coastguard Worker#
3*795d594fSAndroid Build Coastguard Worker# Copyright (C) 2021 The Android Open Source Project
4*795d594fSAndroid Build Coastguard Worker#
5*795d594fSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
6*795d594fSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
7*795d594fSAndroid Build Coastguard Worker# You may obtain a copy of the License at
8*795d594fSAndroid Build Coastguard Worker#
9*795d594fSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
10*795d594fSAndroid Build Coastguard Worker#
11*795d594fSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
12*795d594fSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
13*795d594fSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14*795d594fSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
15*795d594fSAndroid Build Coastguard Worker# limitations under the License.
16*795d594fSAndroid Build Coastguard Worker
17*795d594fSAndroid Build Coastguard Worker#
18*795d594fSAndroid Build Coastguard Worker# Generates profiles from the set of all methods in a given set of dex/jars and
19*795d594fSAndroid Build Coastguard Worker# bisects to find minimal repro sets.
20*795d594fSAndroid Build Coastguard Worker#
21*795d594fSAndroid Build Coastguard Worker
22*795d594fSAndroid Build Coastguard Workerimport shlex
23*795d594fSAndroid Build Coastguard Workerimport argparse
24*795d594fSAndroid Build Coastguard Workerimport pylibdexfile
25*795d594fSAndroid Build Coastguard Workerimport math
26*795d594fSAndroid Build Coastguard Workerimport subprocess
27*795d594fSAndroid Build Coastguard Workerfrom collections import namedtuple
28*795d594fSAndroid Build Coastguard Workerimport sys
29*795d594fSAndroid Build Coastguard Workerimport random
30*795d594fSAndroid Build Coastguard Workerimport os
31*795d594fSAndroid Build Coastguard Worker
32*795d594fSAndroid Build Coastguard WorkerApkEntry = namedtuple("ApkEntry", ["file", "location"])
33*795d594fSAndroid Build Coastguard Worker
34*795d594fSAndroid Build Coastguard Worker
35*795d594fSAndroid Build Coastguard Workerdef get_parser():
36*795d594fSAndroid Build Coastguard Worker  parser = argparse.ArgumentParser(
37*795d594fSAndroid Build Coastguard Worker      description="Bisect profile contents. We will wait while the user runs test"
38*795d594fSAndroid Build Coastguard Worker  )
39*795d594fSAndroid Build Coastguard Worker
40*795d594fSAndroid Build Coastguard Worker  class ApkAction(argparse.Action):
41*795d594fSAndroid Build Coastguard Worker
42*795d594fSAndroid Build Coastguard Worker    def __init__(self, option_strings, dest, **kwargs):
43*795d594fSAndroid Build Coastguard Worker      super(ApkAction, self).__init__(option_strings, dest, **kwargs)
44*795d594fSAndroid Build Coastguard Worker
45*795d594fSAndroid Build Coastguard Worker    def __call__(self, parser, namespace, values, option_string=None):
46*795d594fSAndroid Build Coastguard Worker      lst = getattr(namespace, self.dest)
47*795d594fSAndroid Build Coastguard Worker      if lst is None:
48*795d594fSAndroid Build Coastguard Worker        setattr(namespace, self.dest, [])
49*795d594fSAndroid Build Coastguard Worker        lst = getattr(namespace, self.dest)
50*795d594fSAndroid Build Coastguard Worker      if len(values) == 1:
51*795d594fSAndroid Build Coastguard Worker        values = (values[0], values[0])
52*795d594fSAndroid Build Coastguard Worker      assert len(values) == 2, values
53*795d594fSAndroid Build Coastguard Worker      lst.append(ApkEntry(*values))
54*795d594fSAndroid Build Coastguard Worker
55*795d594fSAndroid Build Coastguard Worker  apks = parser.add_argument_group(title="APK selection")
56*795d594fSAndroid Build Coastguard Worker  apks.add_argument(
57*795d594fSAndroid Build Coastguard Worker      "--apk",
58*795d594fSAndroid Build Coastguard Worker      action=ApkAction,
59*795d594fSAndroid Build Coastguard Worker      dest="apks",
60*795d594fSAndroid Build Coastguard Worker      nargs=1,
61*795d594fSAndroid Build Coastguard Worker      default=[],
62*795d594fSAndroid Build Coastguard Worker      help="an apk/dex/jar to get methods from. Uses same path as location. " +
63*795d594fSAndroid Build Coastguard Worker           "Use --apk-and-location if this isn't desired."
64*795d594fSAndroid Build Coastguard Worker  )
65*795d594fSAndroid Build Coastguard Worker  apks.add_argument(
66*795d594fSAndroid Build Coastguard Worker      "--apk-and-location",
67*795d594fSAndroid Build Coastguard Worker      action=ApkAction,
68*795d594fSAndroid Build Coastguard Worker      nargs=2,
69*795d594fSAndroid Build Coastguard Worker      dest="apks",
70*795d594fSAndroid Build Coastguard Worker      help="an apk/dex/jar + location to get methods from."
71*795d594fSAndroid Build Coastguard Worker  )
72*795d594fSAndroid Build Coastguard Worker  profiles = parser.add_argument_group(
73*795d594fSAndroid Build Coastguard Worker      title="Profile selection").add_mutually_exclusive_group()
74*795d594fSAndroid Build Coastguard Worker  profiles.add_argument(
75*795d594fSAndroid Build Coastguard Worker      "--input-text-profile", help="a text profile to use for bisect")
76*795d594fSAndroid Build Coastguard Worker  profiles.add_argument("--input-profile", help="a profile to use for bisect")
77*795d594fSAndroid Build Coastguard Worker  parser.add_argument(
78*795d594fSAndroid Build Coastguard Worker      "--output-source", help="human readable file create the profile from")
79*795d594fSAndroid Build Coastguard Worker  parser.add_argument("--test-exec", help="file to exec (without arguments) to test a" +
80*795d594fSAndroid Build Coastguard Worker                                           " candidate. Test should exit 0 if the issue" +
81*795d594fSAndroid Build Coastguard Worker                                           " is not present and non-zero if the issue is" +
82*795d594fSAndroid Build Coastguard Worker                                           " present.")
83*795d594fSAndroid Build Coastguard Worker  parser.add_argument("output_file", help="file we will write the profiles to")
84*795d594fSAndroid Build Coastguard Worker  return parser
85*795d594fSAndroid Build Coastguard Worker
86*795d594fSAndroid Build Coastguard Worker
87*795d594fSAndroid Build Coastguard Workerdef dump_files(meths, args, output):
88*795d594fSAndroid Build Coastguard Worker  for m in meths:
89*795d594fSAndroid Build Coastguard Worker    print("HS{}".format(m), file=output)
90*795d594fSAndroid Build Coastguard Worker  output.flush()
91*795d594fSAndroid Build Coastguard Worker  profman_args = [
92*795d594fSAndroid Build Coastguard Worker      "profmand", "--reference-profile-file={}".format(args.output_file),
93*795d594fSAndroid Build Coastguard Worker      "--create-profile-from={}".format(args.output_source)
94*795d594fSAndroid Build Coastguard Worker  ]
95*795d594fSAndroid Build Coastguard Worker  print(" ".join(map(shlex.quote, profman_args)))
96*795d594fSAndroid Build Coastguard Worker  for apk in args.apks:
97*795d594fSAndroid Build Coastguard Worker    profman_args += [
98*795d594fSAndroid Build Coastguard Worker        "--apk={}".format(apk.file), "--dex-location={}".format(apk.location)
99*795d594fSAndroid Build Coastguard Worker    ]
100*795d594fSAndroid Build Coastguard Worker  profman = subprocess.run(profman_args)
101*795d594fSAndroid Build Coastguard Worker  profman.check_returncode()
102*795d594fSAndroid Build Coastguard Worker
103*795d594fSAndroid Build Coastguard Worker
104*795d594fSAndroid Build Coastguard Workerdef get_answer(args):
105*795d594fSAndroid Build Coastguard Worker  if args.test_exec is None:
106*795d594fSAndroid Build Coastguard Worker    while True:
107*795d594fSAndroid Build Coastguard Worker      answer = input("Does the file at {} cause the issue (y/n):".format(
108*795d594fSAndroid Build Coastguard Worker          args.output_file))
109*795d594fSAndroid Build Coastguard Worker      if len(answer) >= 1 and answer[0].lower() == "y":
110*795d594fSAndroid Build Coastguard Worker        return "y"
111*795d594fSAndroid Build Coastguard Worker      elif len(answer) >= 1 and answer[0].lower() == "n":
112*795d594fSAndroid Build Coastguard Worker        return "n"
113*795d594fSAndroid Build Coastguard Worker      else:
114*795d594fSAndroid Build Coastguard Worker        print("Please enter 'y' or 'n' only!")
115*795d594fSAndroid Build Coastguard Worker  else:
116*795d594fSAndroid Build Coastguard Worker    test_args = shlex.split(args.test_exec)
117*795d594fSAndroid Build Coastguard Worker    print(" ".join(map(shlex.quote, test_args)))
118*795d594fSAndroid Build Coastguard Worker    answer = subprocess.run(test_args)
119*795d594fSAndroid Build Coastguard Worker    if answer.returncode == 0:
120*795d594fSAndroid Build Coastguard Worker      return "n"
121*795d594fSAndroid Build Coastguard Worker    else:
122*795d594fSAndroid Build Coastguard Worker      return "y"
123*795d594fSAndroid Build Coastguard Worker
124*795d594fSAndroid Build Coastguard Workerdef run_test(meths, args):
125*795d594fSAndroid Build Coastguard Worker  with open(args.output_source, "wt") as output:
126*795d594fSAndroid Build Coastguard Worker    dump_files(meths, args, output)
127*795d594fSAndroid Build Coastguard Worker    print("Currently testing {} methods. ~{} rounds to go.".format(
128*795d594fSAndroid Build Coastguard Worker        len(meths), 1 + math.floor(math.log2(len(meths)))))
129*795d594fSAndroid Build Coastguard Worker  return get_answer(args)
130*795d594fSAndroid Build Coastguard Worker
131*795d594fSAndroid Build Coastguard Workerdef main():
132*795d594fSAndroid Build Coastguard Worker  parser = get_parser()
133*795d594fSAndroid Build Coastguard Worker  args = parser.parse_args()
134*795d594fSAndroid Build Coastguard Worker  if args.output_source is None:
135*795d594fSAndroid Build Coastguard Worker    fdnum = os.memfd_create("tempfile_profile")
136*795d594fSAndroid Build Coastguard Worker    args.output_source = "/proc/{}/fd/{}".format(os.getpid(), fdnum)
137*795d594fSAndroid Build Coastguard Worker  all_dexs = list()
138*795d594fSAndroid Build Coastguard Worker  for f in args.apks:
139*795d594fSAndroid Build Coastguard Worker    try:
140*795d594fSAndroid Build Coastguard Worker      all_dexs.append(pylibdexfile.FileDexFile(f.file, f.location))
141*795d594fSAndroid Build Coastguard Worker    except Exception as e1:
142*795d594fSAndroid Build Coastguard Worker      try:
143*795d594fSAndroid Build Coastguard Worker        all_dexs += pylibdexfile.OpenJar(f.file)
144*795d594fSAndroid Build Coastguard Worker      except Exception as e2:
145*795d594fSAndroid Build Coastguard Worker        parser.error("Failed to open file: {}. errors were {} and {}".format(
146*795d594fSAndroid Build Coastguard Worker            f.file, e1, e2))
147*795d594fSAndroid Build Coastguard Worker  if args.input_profile is not None:
148*795d594fSAndroid Build Coastguard Worker    profman_args = [
149*795d594fSAndroid Build Coastguard Worker        "profmand", "--dump-classes-and-methods",
150*795d594fSAndroid Build Coastguard Worker        "--profile-file={}".format(args.input_profile)
151*795d594fSAndroid Build Coastguard Worker    ]
152*795d594fSAndroid Build Coastguard Worker    for apk in args.apks:
153*795d594fSAndroid Build Coastguard Worker      profman_args.append("--apk={}".format(apk.file))
154*795d594fSAndroid Build Coastguard Worker    print(" ".join(map(shlex.quote, profman_args)))
155*795d594fSAndroid Build Coastguard Worker    res = subprocess.run(
156*795d594fSAndroid Build Coastguard Worker        profman_args, capture_output=True, universal_newlines=True)
157*795d594fSAndroid Build Coastguard Worker    res.check_returncode()
158*795d594fSAndroid Build Coastguard Worker    meth_list = list(filter(lambda a: a != "", res.stdout.split()))
159*795d594fSAndroid Build Coastguard Worker  elif args.input_text_profile is not None:
160*795d594fSAndroid Build Coastguard Worker    with open(args.input_text_profile, "rt") as inp:
161*795d594fSAndroid Build Coastguard Worker      meth_list = list(filter(lambda a: a != "", inp.readlines()))
162*795d594fSAndroid Build Coastguard Worker  else:
163*795d594fSAndroid Build Coastguard Worker    all_methods = set()
164*795d594fSAndroid Build Coastguard Worker    for d in all_dexs:
165*795d594fSAndroid Build Coastguard Worker      for m in d.methods:
166*795d594fSAndroid Build Coastguard Worker        all_methods.add(m.descriptor)
167*795d594fSAndroid Build Coastguard Worker    meth_list = list(all_methods)
168*795d594fSAndroid Build Coastguard Worker  print("Found {} methods. Will take ~{} iterations".format(
169*795d594fSAndroid Build Coastguard Worker      len(meth_list), 1 + math.floor(math.log2(len(meth_list)))))
170*795d594fSAndroid Build Coastguard Worker  print(
171*795d594fSAndroid Build Coastguard Worker      "type 'yes' if the behavior you are looking for is present (i.e. the compiled code crashes " +
172*795d594fSAndroid Build Coastguard Worker      "or something)"
173*795d594fSAndroid Build Coastguard Worker  )
174*795d594fSAndroid Build Coastguard Worker  print("Performing single check with all methods")
175*795d594fSAndroid Build Coastguard Worker  result = run_test(meth_list, args)
176*795d594fSAndroid Build Coastguard Worker  if result[0].lower() != "y":
177*795d594fSAndroid Build Coastguard Worker    cont = input(
178*795d594fSAndroid Build Coastguard Worker        "The behavior you were looking for did not occur when run against all methods. Continue " +
179*795d594fSAndroid Build Coastguard Worker        "(yes/no)? "
180*795d594fSAndroid Build Coastguard Worker    )
181*795d594fSAndroid Build Coastguard Worker    if cont[0].lower() != "y":
182*795d594fSAndroid Build Coastguard Worker      print("Aborting!")
183*795d594fSAndroid Build Coastguard Worker      sys.exit(1)
184*795d594fSAndroid Build Coastguard Worker  needs_dump = False
185*795d594fSAndroid Build Coastguard Worker  while len(meth_list) > 1:
186*795d594fSAndroid Build Coastguard Worker    test_methods = list(meth_list[0:len(meth_list) // 2])
187*795d594fSAndroid Build Coastguard Worker    result = run_test(test_methods, args)
188*795d594fSAndroid Build Coastguard Worker    if result[0].lower() == "y":
189*795d594fSAndroid Build Coastguard Worker      meth_list = test_methods
190*795d594fSAndroid Build Coastguard Worker      needs_dump = False
191*795d594fSAndroid Build Coastguard Worker    else:
192*795d594fSAndroid Build Coastguard Worker      meth_list = meth_list[len(meth_list) // 2:]
193*795d594fSAndroid Build Coastguard Worker      needs_dump = True
194*795d594fSAndroid Build Coastguard Worker  if needs_dump:
195*795d594fSAndroid Build Coastguard Worker    with open(args.output_source, "wt") as output:
196*795d594fSAndroid Build Coastguard Worker      dump_files(meth_list, args, output)
197*795d594fSAndroid Build Coastguard Worker  print("Found result!")
198*795d594fSAndroid Build Coastguard Worker  print("{}".format(meth_list[0]))
199*795d594fSAndroid Build Coastguard Worker  print("Leaving profile at {} and text profile at {}".format(
200*795d594fSAndroid Build Coastguard Worker      args.output_file, args.output_source))
201*795d594fSAndroid Build Coastguard Worker
202*795d594fSAndroid Build Coastguard Worker
203*795d594fSAndroid Build Coastguard Workerif __name__ == "__main__":
204*795d594fSAndroid Build Coastguard Worker  main()
205