1#!/usr/bin/env python3 2# 3# Copyright 2018, The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17# 18# 19# Query the current compiler filter for an application by its package name. 20# (By parsing the results of the 'adb shell dumpsys package $package' command). 21# The output is a string "$compilation_filter $compilation_reason $isa". 22# 23# See --help for more details. 24# 25# ----------------------------------- 26# 27# Sample usage: 28# 29# $> ./query_compiler_filter.py --package com.google.android.calculator 30# speed-profile unknown arm64 31# 32 33import argparse 34import os 35import re 36import sys 37 38# TODO: refactor this with a common library file with analyze_metrics.py 39DIR = os.path.abspath(os.path.dirname(__file__)) 40sys.path.append(os.path.dirname(DIR)) 41import lib.cmd_utils as cmd_utils 42import lib.print_utils as print_utils 43 44from typing import List, NamedTuple, Iterable 45 46_DEBUG_FORCE = None # Ignore -d/--debug if this is not none. 47 48def parse_options(argv: List[str] = None): 49 """Parse command line arguments and return an argparse Namespace object.""" 50 parser = argparse.ArgumentParser(description="Query the compiler filter for a package.") 51 # argparse considers args starting with - and -- optional in --help, even though required=True. 52 # by using a named argument group --help will clearly say that it's required instead of optional. 53 required_named = parser.add_argument_group('required named arguments') 54 required_named.add_argument('-p', '--package', action='store', dest='package', help='package of the application', required=True) 55 56 # optional arguments 57 # use a group here to get the required arguments to appear 'above' the optional arguments in help. 58 optional_named = parser.add_argument_group('optional named arguments') 59 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')) 60 optional_named.add_argument('-s', '--simulate', dest='simulate', action='store_true', help='Print which commands will run, but don\'t run the apps') 61 optional_named.add_argument('-d', '--debug', dest='debug', action='store_true', help='Add extra debugging output') 62 63 return parser.parse_args(argv) 64 65def remote_dumpsys_package(package: str, simulate: bool) -> str: 66 # --simulate is used for interactive debugging/development, but also for the unit test. 67 if simulate: 68 return """ 69Dexopt state: 70 [%s] 71 path: /data/app/%s-D7s8PLidqqEq7Jc7UH_a5A==/base.apk 72 arm64: [status=speed-profile] [reason=unknown] 73 path: /data/app/%s-D7s8PLidqqEq7Jc7UH_a5A==/base.apk 74 arm: [status=speed] [reason=first-boot] 75 path: /data/app/%s-D7s8PLidqqEq7Jc7UH_a5A==/base.apk 76 x86: [status=quicken] [reason=install] 77""" %(package, package, package, package) 78 79 code, res = cmd_utils.execute_arbitrary_command(['adb', 'shell', 'dumpsys', 80 'package', package], 81 simulate=False, 82 timeout=5, 83 shell=False) 84 if code: 85 return res 86 else: 87 raise AssertionError("Failed to dumpsys package, errors = %s", res) 88 89ParseTree = NamedTuple('ParseTree', [('label', str), ('children', List['ParseTree'])]) 90DexoptState = ParseTree # With the Dexopt state: label 91ParseResult = NamedTuple('ParseResult', [('remainder', List[str]), ('tree', ParseTree)]) 92 93def find_parse_subtree(parse_tree: ParseTree, match_regex: str) -> ParseTree: 94 if re.match(match_regex, parse_tree.label): 95 return parse_tree 96 97 for node in parse_tree.children: 98 res = find_parse_subtree(node, match_regex) 99 if res: 100 return res 101 102 return None 103 104def find_parse_children(parse_tree: ParseTree, match_regex: str) -> Iterable[ParseTree]: 105 for node in parse_tree.children: 106 if re.match(match_regex, node.label): 107 yield node 108 109def parse_tab_subtree(label: str, str_lines: List[str], separator=' ', indent=-1) -> ParseResult: 110 children = [] 111 112 get_indent_level = lambda line: len(line) - len(line.lstrip()) 113 114 line_num = 0 115 116 keep_going = True 117 while keep_going: 118 keep_going = False 119 120 for line_num in range(len(str_lines)): 121 line = str_lines[line_num] 122 current_indent = get_indent_level(line) 123 124 print_utils.debug_print("INDENT=%d, LINE=%s" %(current_indent, line)) 125 126 current_label = line.lstrip() 127 128 # skip empty lines 129 if line.lstrip() == "": 130 continue 131 132 if current_indent > indent: 133 parse_result = parse_tab_subtree(current_label, str_lines[line_num+1::], separator, current_indent) 134 str_lines = parse_result.remainder 135 children.append(parse_result.tree) 136 keep_going = True 137 else: 138 # current_indent <= indent 139 keep_going = False 140 141 break 142 143 new_remainder = str_lines[line_num::] 144 print_utils.debug_print("NEW REMAINDER: ", new_remainder) 145 146 parse_tree = ParseTree(label, children) 147 return ParseResult(new_remainder, parse_tree) 148 149def parse_tab_tree(str_tree: str, separator=' ', indentation_level=-1) -> ParseTree: 150 151 label = None 152 lst = [] 153 154 line_num = 0 155 line_lst = str_tree.split("\n") 156 157 return parse_tab_subtree("", line_lst, separator, indentation_level).tree 158 159def parse_dexopt_state(dumpsys_tree: ParseTree) -> DexoptState: 160 res = find_parse_subtree(dumpsys_tree, "Dexopt(\s+)state[:]?") 161 if not res: 162 raise AssertionError("Could not find the Dexopt state") 163 return res 164 165def find_first_compiler_filter(dexopt_state: DexoptState, package: str, instruction_set: str) -> str: 166 lst = find_all_compiler_filters(dexopt_state, package) 167 168 print_utils.debug_print("all compiler filters: ", lst) 169 170 for compiler_filter_info in lst: 171 if not instruction_set: 172 return compiler_filter_info 173 174 if compiler_filter_info.isa == instruction_set: 175 return compiler_filter_info 176 177 return None 178 179CompilerFilterInfo = NamedTuple('CompilerFilterInfo', [('isa', str), ('status', str), ('reason', str)]) 180 181def find_all_compiler_filters(dexopt_state: DexoptState, package: str) -> List[CompilerFilterInfo]: 182 183 lst = [] 184 package_tree = find_parse_subtree(dexopt_state, re.escape("[%s]" %package)) 185 186 if not package_tree: 187 raise AssertionError("Could not find any package subtree for package %s" %(package)) 188 189 print_utils.debug_print("package tree: ", package_tree) 190 191 for path_tree in find_parse_children(package_tree, "path: "): 192 print_utils.debug_print("path tree: ", path_tree) 193 194 matchre = re.compile("([^:]+):\s+\[status=([^\]]+)\]\s+\[reason=([^\]]+)\]") 195 196 for isa_node in find_parse_children(path_tree, matchre): 197 198 matches = re.match(matchre, isa_node.label).groups() 199 200 info = CompilerFilterInfo(*matches) 201 lst.append(info) 202 203 return lst 204 205def main() -> int: 206 opts = parse_options() 207 cmd_utils._debug = opts.debug 208 if _DEBUG_FORCE is not None: 209 cmd_utils._debug = _DEBUG_FORCE 210 print_utils.debug_print("parsed options: ", opts) 211 212 # Note: This can often 'fail' if the package isn't actually installed. 213 package_dumpsys = remote_dumpsys_package(opts.package, opts.simulate) 214 print_utils.debug_print("package dumpsys: ", package_dumpsys) 215 dumpsys_parse_tree = parse_tab_tree(package_dumpsys, package_dumpsys) 216 print_utils.debug_print("parse tree: ", dumpsys_parse_tree) 217 dexopt_state = parse_dexopt_state(dumpsys_parse_tree) 218 219 filter = find_first_compiler_filter(dexopt_state, opts.package, opts.instruction_set) 220 221 if filter: 222 print(filter.status, end=' ') 223 print(filter.reason, end=' ') 224 print(filter.isa) 225 else: 226 print("ERROR: Could not find any compiler-filter for package %s, isa %s" %(opts.package, opts.instruction_set), file=sys.stderr) 227 return 1 228 229 return 0 230 231if __name__ == '__main__': 232 sys.exit(main()) 233