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