| #!/usr/bin/env python3 |
| # |
| # Copyright 2018, 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. |
| |
| # |
| # |
| # Query the current compiler filter for an application by its package name. |
| # (By parsing the results of the 'adb shell dumpsys package $package' command). |
| # The output is a string "$compilation_filter $compilation_reason $isa". |
| # |
| # See --help for more details. |
| # |
| # ----------------------------------- |
| # |
| # Sample usage: |
| # |
| # $> ./query_compiler_filter.py --package com.google.android.calculator |
| # speed-profile unknown arm64 |
| # |
| |
| import argparse |
| import sys |
| import re |
| |
| # TODO: refactor this with a common library file with analyze_metrics.py |
| import app_startup_runner |
| from app_startup_runner import _debug_print |
| from app_startup_runner import execute_arbitrary_command |
| |
| from typing import List, NamedTuple, Iterable |
| |
| _DEBUG_FORCE = None # Ignore -d/--debug if this is not none. |
| |
| def parse_options(argv: List[str] = None): |
| """Parse command line arguments and return an argparse Namespace object.""" |
| parser = argparse.ArgumentParser(description="Query the compiler filter for a package.") |
| # argparse considers args starting with - and -- optional in --help, even though required=True. |
| # by using a named argument group --help will clearly say that it's required instead of optional. |
| required_named = parser.add_argument_group('required named arguments') |
| required_named.add_argument('-p', '--package', action='store', dest='package', help='package of the application', required=True) |
| |
| # optional arguments |
| # use a group here to get the required arguments to appear 'above' the optional arguments in help. |
| optional_named = parser.add_argument_group('optional named arguments') |
| optional_named.add_argument('-i', '--isa', '--instruction-set', action='store', dest='instruction_set', help='which instruction set to select. defaults to the first one available if not specified.', choices=('arm64', 'arm', 'x86_64', 'x86')) |
| optional_named.add_argument('-s', '--simulate', dest='simulate', action='store_true', help='Print which commands will run, but don\'t run the apps') |
| optional_named.add_argument('-d', '--debug', dest='debug', action='store_true', help='Add extra debugging output') |
| |
| return parser.parse_args(argv) |
| |
| def remote_dumpsys_package(package: str, simulate: bool) -> str: |
| # --simulate is used for interactive debugging/development, but also for the unit test. |
| if simulate: |
| return """ |
| Dexopt state: |
| [%s] |
| path: /data/app/%s-D7s8PLidqqEq7Jc7UH_a5A==/base.apk |
| arm64: [status=speed-profile] [reason=unknown] |
| path: /data/app/%s-D7s8PLidqqEq7Jc7UH_a5A==/base.apk |
| arm: [status=speed] [reason=first-boot] |
| path: /data/app/%s-D7s8PLidqqEq7Jc7UH_a5A==/base.apk |
| x86: [status=quicken] [reason=install] |
| """ %(package, package, package, package) |
| |
| code, res = execute_arbitrary_command(['adb', 'shell', 'dumpsys', 'package', package], simulate=False, timeout=5) |
| if code: |
| return res |
| else: |
| raise AssertionError("Failed to dumpsys package, errors = %s", res) |
| |
| ParseTree = NamedTuple('ParseTree', [('label', str), ('children', List['ParseTree'])]) |
| DexoptState = ParseTree # With the Dexopt state: label |
| ParseResult = NamedTuple('ParseResult', [('remainder', List[str]), ('tree', ParseTree)]) |
| |
| def find_parse_subtree(parse_tree: ParseTree, match_regex: str) -> ParseTree: |
| if re.match(match_regex, parse_tree.label): |
| return parse_tree |
| |
| for node in parse_tree.children: |
| res = find_parse_subtree(node, match_regex) |
| if res: |
| return res |
| |
| return None |
| |
| def find_parse_children(parse_tree: ParseTree, match_regex: str) -> Iterable[ParseTree]: |
| for node in parse_tree.children: |
| if re.match(match_regex, node.label): |
| yield node |
| |
| def parse_tab_subtree(label: str, str_lines: List[str], separator=' ', indent=-1) -> ParseResult: |
| children = [] |
| |
| get_indent_level = lambda line: len(line) - len(line.lstrip()) |
| |
| line_num = 0 |
| |
| keep_going = True |
| while keep_going: |
| keep_going = False |
| |
| for line_num in range(len(str_lines)): |
| line = str_lines[line_num] |
| current_indent = get_indent_level(line) |
| |
| _debug_print("INDENT=%d, LINE=%s" %(current_indent, line)) |
| |
| current_label = line.lstrip() |
| |
| # skip empty lines |
| if line.lstrip() == "": |
| continue |
| |
| if current_indent > indent: |
| parse_result = parse_tab_subtree(current_label, str_lines[line_num+1::], separator, current_indent) |
| str_lines = parse_result.remainder |
| children.append(parse_result.tree) |
| keep_going = True |
| else: |
| # current_indent <= indent |
| keep_going = False |
| |
| break |
| |
| new_remainder = str_lines[line_num::] |
| _debug_print("NEW REMAINDER: ", new_remainder) |
| |
| parse_tree = ParseTree(label, children) |
| return ParseResult(new_remainder, parse_tree) |
| |
| def parse_tab_tree(str_tree: str, separator=' ', indentation_level=-1) -> ParseTree: |
| |
| label = None |
| lst = [] |
| |
| line_num = 0 |
| line_lst = str_tree.split("\n") |
| |
| return parse_tab_subtree("", line_lst, separator, indentation_level).tree |
| |
| def parse_dexopt_state(dumpsys_tree: ParseTree) -> DexoptState: |
| res = find_parse_subtree(dumpsys_tree, "Dexopt(\s+)state[:]?") |
| if not res: |
| raise AssertionError("Could not find the Dexopt state") |
| return res |
| |
| def find_first_compiler_filter(dexopt_state: DexoptState, package: str, instruction_set: str) -> str: |
| lst = find_all_compiler_filters(dexopt_state, package) |
| |
| _debug_print("all compiler filters: ", lst) |
| |
| for compiler_filter_info in lst: |
| if not instruction_set: |
| return compiler_filter_info |
| |
| if compiler_filter_info.isa == instruction_set: |
| return compiler_filter_info |
| |
| return None |
| |
| CompilerFilterInfo = NamedTuple('CompilerFilterInfo', [('isa', str), ('status', str), ('reason', str)]) |
| |
| def find_all_compiler_filters(dexopt_state: DexoptState, package: str) -> List[CompilerFilterInfo]: |
| |
| lst = [] |
| package_tree = find_parse_subtree(dexopt_state, re.escape("[%s]" %package)) |
| |
| if not package_tree: |
| raise AssertionError("Could not find any package subtree for package %s" %(package)) |
| |
| _debug_print("package tree: ", package_tree) |
| |
| for path_tree in find_parse_children(package_tree, "path: "): |
| _debug_print("path tree: ", path_tree) |
| |
| matchre = re.compile("([^:]+):\s+\[status=([^\]]+)\]\s+\[reason=([^\]]+)\]") |
| |
| for isa_node in find_parse_children(path_tree, matchre): |
| |
| matches = re.match(matchre, isa_node.label).groups() |
| |
| info = CompilerFilterInfo(*matches) |
| lst.append(info) |
| |
| return lst |
| |
| def main() -> int: |
| opts = parse_options() |
| app_startup_runner._debug = opts.debug |
| if _DEBUG_FORCE is not None: |
| app_startup_runner._debug = _DEBUG_FORCE |
| _debug_print("parsed options: ", opts) |
| |
| # Note: This can often 'fail' if the package isn't actually installed. |
| package_dumpsys = remote_dumpsys_package(opts.package, opts.simulate) |
| _debug_print("package dumpsys: ", package_dumpsys) |
| dumpsys_parse_tree = parse_tab_tree(package_dumpsys, package_dumpsys) |
| _debug_print("parse tree: ", dumpsys_parse_tree) |
| dexopt_state = parse_dexopt_state(dumpsys_parse_tree) |
| |
| filter = find_first_compiler_filter(dexopt_state, opts.package, opts.instruction_set) |
| |
| if filter: |
| print(filter.status, end=' ') |
| print(filter.reason, end=' ') |
| print(filter.isa) |
| else: |
| print("ERROR: Could not find any compiler-filter for package %s, isa %s" %(opts.package, opts.instruction_set), file=sys.stderr) |
| return 1 |
| |
| return 0 |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |