diff options
Diffstat (limited to 'tools/bisect_profile.py')
-rwxr-xr-x | tools/bisect_profile.py | 189 |
1 files changed, 189 insertions, 0 deletions
diff --git a/tools/bisect_profile.py b/tools/bisect_profile.py new file mode 100755 index 0000000000..9b2479a2a7 --- /dev/null +++ b/tools/bisect_profile.py @@ -0,0 +1,189 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2021 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Generates profiles from the set of all methods in a given set of dex/jars and +# bisects to find minimal repro sets. +# + +import shlex +import argparse +import pylibdexfile +import math +import subprocess +from collections import namedtuple +import sys +import random +import os + +ApkEntry = namedtuple("ApkEntry", ["file", "location"]) + + +def get_parser(): + parser = argparse.ArgumentParser( + description="Bisect profile contents. We will wait while the user runs test" + ) + + class ApkAction(argparse.Action): + + def __init__(self, option_strings, dest, **kwargs): + super(ApkAction, self).__init__(option_strings, dest, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + lst = getattr(namespace, self.dest) + if lst is None: + setattr(namespace, self.dest, []) + lst = getattr(namespace, self.dest) + if len(values) == 1: + values = (values[0], values[0]) + assert len(values) == 2, values + lst.append(ApkEntry(*values)) + + apks = parser.add_argument_group(title="APK selection") + apks.add_argument( + "--apk", + action=ApkAction, + dest="apks", + nargs=1, + default=[], + help="an apk/dex/jar to get methods from. Uses same path as location. " + + "Use --apk-and-location if this isn't desired." + ) + apks.add_argument( + "--apk-and-location", + action=ApkAction, + nargs=2, + dest="apks", + help="an apk/dex/jar + location to get methods from." + ) + profiles = parser.add_argument_group( + title="Profile selection").add_mutually_exclusive_group() + profiles.add_argument( + "--input-text-profile", help="a text profile to use for bisect") + profiles.add_argument("--input-profile", help="a profile to use for bisect") + parser.add_argument( + "--output-source", help="human readable file create the profile from") + parser.add_argument("output_file", help="file we will write the profiles to") + return parser + + +def dump_files(meths, args, output): + for m in meths: + print("HS{}".format(m), file=output) + output.flush() + profman_args = [ + "profmand", "--reference-profile-file={}".format(args.output_file), + "--create-profile-from={}".format(args.output_source) + ] + print(" ".join(map(shlex.quote, profman_args))) + for apk in args.apks: + profman_args += [ + "--apk={}".format(apk.file), "--dex-location={}".format(apk.location) + ] + profman = subprocess.run(profman_args) + profman.check_returncode() + + +def run_test(meths, args): + with open(args.output_source, "wt") as output: + dump_files(meths, args, output) + print("Currently testing {} methods. ~{} rounds to go.".format( + len(meths), 1 + math.floor(math.log2(len(meths))))) + while True: + answer = input("Does the file at {} cause the issue (y/n):".format( + args.output_file)) + if answer[0].lower() == "y": + return "y" + elif answer[0].lower() == "n": + return "n" + else: + print("Please enter 'y' or 'n' only!") + + +def main(): + parser = get_parser() + args = parser.parse_args() + if args.output_source is None: + fdnum = os.memfd_create("tempfile_profile") + args.output_source = "/proc/{}/fd/{}".format(os.getpid(), fdnum) + all_dexs = list() + for f in args.apks: + try: + all_dexs.append(pylibdexfile.FileDexFile(f.file, f.location)) + except Exception as e1: + try: + all_dexs += pylibdexfile.OpenJar(f.file) + except Exception as e2: + parser.error("Failed to open file: {}. errors were {} and {}".format( + f.file, e1, e2)) + if args.input_profile is not None: + profman_args = [ + "profmand", "--dump-classes-and-methods", + "--profile-file={}".format(args.input_profile) + ] + for apk in args.apks: + profman_args.append("--apk={}".format(apk.file)) + print(" ".join(map(shlex.quote, profman_args))) + res = subprocess.run( + profman_args, capture_output=True, universal_newlines=True) + res.check_returncode() + meth_list = list(filter(lambda a: a != "", res.stdout.split())) + elif args.input_text_profile is not None: + with open(args.input_text_profile, "rt") as inp: + meth_list = list(filter(lambda a: a != "", inp.readlines())) + else: + all_methods = set() + for d in all_dexs: + for m in d.methods: + all_methods.add(m.descriptor) + meth_list = list(all_methods) + print("Found {} methods. Will take ~{} iterations".format( + len(meth_list), 1 + math.floor(math.log2(len(meth_list))))) + print( + "type 'yes' if the behavior you are looking for is present (i.e. the compiled code crashes " + + "or something)" + ) + print("Performing single check with all methods") + result = run_test(meth_list, args) + if result[0].lower() != "y": + cont = input( + "The behavior you were looking for did not occur when run against all methods. Continue " + + "(yes/no)? " + ) + if cont[0].lower() != "y": + print("Aborting!") + sys.exit(1) + needs_dump = False + while len(meth_list) > 1: + test_methods = list(meth_list[0:len(meth_list) // 2]) + result = run_test(test_methods, args) + if result[0].lower() == "y": + meth_list = test_methods + needs_dump = False + else: + meth_list = meth_list[len(meth_list) // 2:] + needs_dump = True + if needs_dump: + with open(args.output_source, "wt") as output: + dump_files(meth_list, args, output) + print("Found result!") + print("{}".format(meth_list[0])) + print("Leaving profile at {} and text profile at {}".format( + args.output_file, args.output_source)) + + +if __name__ == "__main__": + main() |