blob: d6c1749b60260423f6bb8a3039829c964d61f20b [file] [log] [blame]
Wojciech Staszkiewicz0fa3cbd2016-08-11 14:04:20 -07001#!/usr/bin/env python3.4
2#
3# Copyright (C) 2016 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"""Performs bisection bug search on methods and optimizations.
18
19See README.md.
20
21Example usage:
22./bisection-search.py -cp classes.dex --expected-output output Test
23"""
24
25import argparse
26import re
27import sys
28
29from common import DeviceTestEnv
30from common import FatalError
31from common import GetEnvVariableOrError
32from common import HostTestEnv
33
34# Passes that are never disabled during search process because disabling them
35# would compromise correctness.
36MANDATORY_PASSES = ['dex_cache_array_fixups_arm',
37 'dex_cache_array_fixups_mips',
38 'instruction_simplifier$before_codegen',
39 'pc_relative_fixups_x86',
40 'pc_relative_fixups_mips',
41 'x86_memory_operand_generation']
42
43# Passes that show up as optimizations in compiler verbose output but aren't
44# driven by run-passes mechanism. They are mandatory and will always run, we
45# never pass them to --run-passes.
46NON_PASSES = ['builder', 'prepare_for_register_allocation',
47 'liveness', 'register']
48
49
50class Dex2OatWrapperTestable(object):
51 """Class representing a testable compilation.
52
53 Accepts filters on compiled methods and optimization passes.
54 """
55
56 def __init__(self, base_cmd, test_env, class_name, args,
57 expected_output=None, verbose=False):
58 """Constructor.
59
60 Args:
61 base_cmd: list of strings, base command to run.
62 test_env: ITestEnv.
63 class_name: string, name of class to run.
64 args: list of strings, program arguments to pass.
65 expected_output: string, expected output to compare against or None.
66 verbose: bool, enable verbose output.
67 """
68 self._base_cmd = base_cmd
69 self._test_env = test_env
70 self._class_name = class_name
71 self._args = args
72 self._expected_output = expected_output
73 self._compiled_methods_path = self._test_env.CreateFile('compiled_methods')
74 self._passes_to_run_path = self._test_env.CreateFile('run_passes')
75 self._verbose = verbose
76
77 def Test(self, compiled_methods, passes_to_run=None):
78 """Tests compilation with compiled_methods and run_passes switches active.
79
80 If compiled_methods is None then compiles all methods.
81 If passes_to_run is None then runs default passes.
82
83 Args:
84 compiled_methods: list of strings representing methods to compile or None.
85 passes_to_run: list of strings representing passes to run or None.
86
87 Returns:
88 True if test passes with given settings. False otherwise.
89 """
90 if self._verbose:
91 print('Testing methods: {0} passes:{1}.'.format(
92 compiled_methods, passes_to_run))
93 cmd = self._PrepareCmd(compiled_methods=compiled_methods,
94 passes_to_run=passes_to_run,
95 verbose_compiler=True)
96 (output, _, ret_code) = self._test_env.RunCommand(cmd)
97 res = ret_code == 0 and (self._expected_output is None
98 or output == self._expected_output)
99 if self._verbose:
100 print('Test passed: {0}.'.format(res))
101 return res
102
103 def GetAllMethods(self):
104 """Get methods compiled during the test.
105
106 Returns:
107 List of strings representing methods compiled during the test.
108
109 Raises:
110 FatalError: An error occurred when retrieving methods list.
111 """
112 cmd = self._PrepareCmd(verbose_compiler=True)
113 (_, err_output, _) = self._test_env.RunCommand(cmd)
114 match_methods = re.findall(r'Building ([^\n]+)\n', err_output)
115 if not match_methods:
116 raise FatalError('Failed to retrieve methods list. '
117 'Not recognized output format.')
118 return match_methods
119
120 def GetAllPassesForMethod(self, compiled_method):
121 """Get all optimization passes ran for a method during the test.
122
123 Args:
124 compiled_method: string representing method to compile.
125
126 Returns:
127 List of strings representing passes ran for compiled_method during test.
128
129 Raises:
130 FatalError: An error occurred when retrieving passes list.
131 """
132 cmd = self._PrepareCmd(compiled_methods=[compiled_method],
133 verbose_compiler=True)
134 (_, err_output, _) = self._test_env.RunCommand(cmd)
135 match_passes = re.findall(r'Starting pass: ([^\n]+)\n', err_output)
136 if not match_passes:
137 raise FatalError('Failed to retrieve passes list. '
138 'Not recognized output format.')
139 return [p for p in match_passes if p not in NON_PASSES]
140
141 def _PrepareCmd(self, compiled_methods=None, passes_to_run=None,
142 verbose_compiler=False):
143 """Prepare command to run."""
144 cmd = list(self._base_cmd)
145 if compiled_methods is not None:
146 self._test_env.WriteLines(self._compiled_methods_path, compiled_methods)
147 cmd += ['-Xcompiler-option', '--compiled-methods={0}'.format(
148 self._compiled_methods_path)]
149 if passes_to_run is not None:
150 self._test_env.WriteLines(self._passes_to_run_path, passes_to_run)
151 cmd += ['-Xcompiler-option', '--run-passes={0}'.format(
152 self._passes_to_run_path)]
153 if verbose_compiler:
154 cmd += ['-Xcompiler-option', '--runtime-arg', '-Xcompiler-option',
155 '-verbose:compiler']
156 cmd += ['-classpath', self._test_env.classpath, self._class_name]
157 cmd += self._args
158 return cmd
159
160
161def BinarySearch(start, end, test):
162 """Binary search integers using test function to guide the process."""
163 while start < end:
164 mid = (start + end) // 2
165 if test(mid):
166 start = mid + 1
167 else:
168 end = mid
169 return start
170
171
172def FilterPasses(passes, cutoff_idx):
173 """Filters passes list according to cutoff_idx but keeps mandatory passes."""
174 return [opt_pass for idx, opt_pass in enumerate(passes)
175 if opt_pass in MANDATORY_PASSES or idx < cutoff_idx]
176
177
178def BugSearch(testable):
179 """Find buggy (method, optimization pass) pair for a given testable.
180
181 Args:
182 testable: Dex2OatWrapperTestable.
183
184 Returns:
185 (string, string) tuple. First element is name of method which when compiled
186 exposes test failure. Second element is name of optimization pass such that
187 for aforementioned method running all passes up to and excluding the pass
188 results in test passing but running all passes up to and including the pass
189 results in test failing.
190
191 (None, None) if test passes when compiling all methods.
192 (string, None) if a method is found which exposes the failure, but the
193 failure happens even when running just mandatory passes.
194
195 Raises:
196 FatalError: Testable fails with no methods compiled.
197 AssertionError: Method failed for all passes when bisecting methods, but
198 passed when bisecting passes. Possible sporadic failure.
199 """
200 all_methods = testable.GetAllMethods()
201 faulty_method_idx = BinarySearch(
202 0,
203 len(all_methods),
204 lambda mid: testable.Test(all_methods[0:mid]))
205 if faulty_method_idx == len(all_methods):
206 return (None, None)
207 if faulty_method_idx == 0:
208 raise FatalError('Testable fails with no methods compiled. '
209 'Perhaps issue lies outside of compiler.')
210 faulty_method = all_methods[faulty_method_idx - 1]
211 all_passes = testable.GetAllPassesForMethod(faulty_method)
212 faulty_pass_idx = BinarySearch(
213 0,
214 len(all_passes),
215 lambda mid: testable.Test([faulty_method],
216 FilterPasses(all_passes, mid)))
217 if faulty_pass_idx == 0:
218 return (faulty_method, None)
219 assert faulty_pass_idx != len(all_passes), 'Method must fail for some passes.'
220 faulty_pass = all_passes[faulty_pass_idx - 1]
221 return (faulty_method, faulty_pass)
222
223
224def PrepareParser():
225 """Prepares argument parser."""
226 parser = argparse.ArgumentParser()
227 parser.add_argument(
228 '-cp', '--classpath', required=True, type=str, help='classpath')
229 parser.add_argument('--expected-output', type=str,
230 help='file containing expected output')
231 parser.add_argument(
232 '--device', action='store_true', default=False, help='run on device')
233 parser.add_argument('classname', type=str, help='name of class to run')
234 parser.add_argument('--lib', dest='lib', type=str, default='libart.so',
235 help='lib to use, default: libart.so')
236 parser.add_argument('--64', dest='x64', action='store_true',
237 default=False, help='x64 mode')
238 parser.add_argument('--dalvikvm-option', dest='dalvikvm_opts',
239 metavar='OPTION', nargs='*', default=[],
240 help='additional dalvikvm option')
241 parser.add_argument('--arg', dest='test_args', nargs='*', default=[],
242 help='argument to pass to program')
243 parser.add_argument('--image', type=str, help='path to image')
244 parser.add_argument('--verbose', action='store_true',
245 default=False, help='enable verbose output')
246 return parser
247
248
249def main():
250 # Parse arguments
251 parser = PrepareParser()
252 args = parser.parse_args()
253
254 # Prepare environment
255 if args.expected_output is not None:
256 with open(args.expected_output, 'r') as f:
257 expected_output = f.read()
258 else:
259 expected_output = None
260 if args.device:
261 run_cmd = ['dalvikvm64'] if args.x64 else ['dalvikvm32']
262 test_env = DeviceTestEnv(args.classpath)
263 else:
264 run_cmd = ['dalvikvm64'] if args.x64 else ['dalvikvm32']
265 run_cmd += ['-XXlib:{0}'.format(args.lib)]
266 if not args.image:
267 image_path = '{0}/framework/core-optimizing-pic.art'.format(
268 GetEnvVariableOrError('ANDROID_HOST_OUT'))
269 else:
270 image_path = args.image
271 run_cmd += ['-Ximage:{0}'.format(image_path)]
272 if args.dalvikvm_opts:
273 run_cmd += args.dalvikvm_opts
274 test_env = HostTestEnv(args.classpath, args.x64)
275
276 # Perform the search
277 try:
278 testable = Dex2OatWrapperTestable(run_cmd, test_env, args.classname,
279 args.test_args, expected_output,
280 args.verbose)
281 (method, opt_pass) = BugSearch(testable)
282 except Exception as e:
283 print('Error. Refer to logfile: {0}'.format(test_env.logfile.name))
284 test_env.logfile.write('Exception: {0}\n'.format(e))
285 raise
286
287 # Report results
288 if method is None:
289 print('Couldn\'t find any bugs.')
290 elif opt_pass is None:
291 print('Faulty method: {0}. Fails with just mandatory passes.'.format(
292 method))
293 else:
294 print('Faulty method and pass: {0}, {1}.'.format(method, opt_pass))
295 print('Logfile: {0}'.format(test_env.logfile.name))
296 sys.exit(0)
297
298
299if __name__ == '__main__':
300 main()