blob: 5fc8fd54139eac75a639b4ec67ec30ffaa14027b [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wu88875db2017-07-20 10:47:53 +08002# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Bisect command line interface."""
6
7from __future__ import print_function
8import argparse
9import datetime
Kuang-che Wu8b654092018-11-09 17:56:25 +080010import json
Kuang-che Wu88875db2017-07-20 10:47:53 +080011import logging
12import os
13import re
Kuang-che Wu443633f2019-02-27 00:58:33 +080014import signal
Kuang-che Wu88875db2017-07-20 10:47:53 +080015import textwrap
16import time
17
18from bisect_kit import common
Kuang-che Wu385279d2017-09-27 14:48:28 +080019from bisect_kit import configure
Kuang-che Wu88875db2017-07-20 10:47:53 +080020from bisect_kit import core
Kuang-che Wue121fae2018-11-09 16:18:39 +080021from bisect_kit import errors
Kuang-che Wu88875db2017-07-20 10:47:53 +080022from bisect_kit import strategy
23from bisect_kit import util
24
25logger = logging.getLogger(__name__)
26
Kuang-che Wu88875db2017-07-20 10:47:53 +080027DEFAULT_SESSION_NAME = 'default'
28DEFAULT_CONFIDENCE = 0.999
29
Kuang-che Wu0476d1f2019-03-04 19:27:01 +080030# Exit code of bisect eval script. These values are chosen compatible with 'git
31# bisect'.
32EXIT_CODE_OLD = 0
33EXIT_CODE_NEW = 1
34EXIT_CODE_SKIP = 125
35EXIT_CODE_FATAL = 128
36
Kuang-che Wu88875db2017-07-20 10:47:53 +080037
38class ArgTypeError(argparse.ArgumentTypeError):
39 """An error for argument validation failure.
40
41 This not only tells users the argument is wrong but also gives correct
42 example. The main purpose of this error is for argtype_multiplexer, which
43 cascades examples from multiple ArgTypeError.
44 """
45
46 def __init__(self, msg, example):
47 self.msg = msg
48 if isinstance(example, list):
49 self.example = example
50 else:
51 self.example = [example]
52 full_msg = '%s (example value: %s)' % (self.msg, ', '.join(self.example))
53 super(ArgTypeError, self).__init__(full_msg)
54
55
56def argtype_notempty(s):
57 """Validates argument is not an empty string.
58
59 Args:
60 s: string to validate.
61
62 Raises:
63 ArgTypeError if argument is empty string.
64 """
65 if not s:
66 msg = 'should not be empty'
67 raise ArgTypeError(msg, 'foo')
68 return s
69
70
71def argtype_int(s):
72 """Validate argument is a number.
73
74 Args:
75 s: string to validate.
76
77 Raises:
78 ArgTypeError if argument is not a number.
79 """
80 try:
81 return str(int(s))
82 except ValueError:
83 raise ArgTypeError('should be a number', '123')
84
85
Kuang-che Wu603cdad2019-01-18 21:32:55 +080086def argtype_re(pattern, example):
87 r"""Validate argument matches `pattern`.
88
89 Args:
90 pattern: regex pattern
91 example: example string which matches `pattern`
92
93 Returns:
94 A new argtype function which matches regex `pattern`
95 """
96 assert re.match(pattern, example)
97
98 def validate(s):
99 if re.match(pattern, s):
100 return s
101 if re.escape(pattern) == pattern:
102 raise ArgTypeError('should be "%s"' % pattern, pattern)
103 raise ArgTypeError('should match "%s"' % pattern,
104 '"%s" like %s' % (pattern, example))
105
106 return validate
107
108
Kuang-che Wu88875db2017-07-20 10:47:53 +0800109def argtype_multiplexer(*args):
110 r"""argtype multiplexer
111
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800112 This function takes a list of argtypes and creates a new function matching
113 them. Moreover, it gives error message with examples.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800114
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800115 Examples:
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800116 >>> argtype = argtype_multiplexer(argtype_int,
117 argtype_re(r'^r\d+$', 'r123'))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800118 >>> argtype('123')
119 123
120 >>> argtype('r456')
121 r456
122 >>> argtype('hello')
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800123 ArgTypeError: Invalid argument (example value: 123, "r\d+$" like r123)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800124
125 Args:
126 *args: list of argtypes or regex pattern.
127
128 Returns:
129 A new argtype function which matches *args.
130 """
131
132 def validate(s):
133 examples = []
134 for t in args:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800135 try:
136 return t(s)
137 except ArgTypeError as e:
138 examples += e.example
139
140 msg = 'Invalid argument'
141 raise ArgTypeError(msg, examples)
142
143 return validate
144
145
146def argtype_multiplier(argtype):
147 """A new argtype that supports multiplier suffix of the given argtype.
148
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800149 Examples:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800150 Supports the given argtype accepting "foo" as argument, this function
151 generates a new argtype function which accepts argument like "foo*3".
152
153 Returns:
154 A new argtype function which returns (arg, times) where arg is accepted
155 by input `argtype` and times is repeating count. Note that if multiplier is
156 omitted, "times" is 1.
157 """
158
159 def helper(s):
160 m = re.match(r'^(.+)\*(\d+)$', s)
161 try:
162 if m:
163 return argtype(m.group(1)), int(m.group(2))
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800164 return argtype(s), 1
Kuang-che Wu88875db2017-07-20 10:47:53 +0800165 except ArgTypeError as e:
166 # It should be okay to gives multiplier example only for the first one
167 # because it is just "example", no need to enumerate all possibilities.
168 raise ArgTypeError(e.msg, e.example + [e.example[0] + '*3'])
169
170 return helper
171
172
173def argtype_dir_path(s):
174 """Validate argument is an existing directory.
175
176 Args:
177 s: string to validate.
178
179 Raises:
180 ArgTypeError if the path is not a directory.
181 """
182 if not os.path.exists(s):
183 raise ArgTypeError('should be an existing directory', '/path/to/somewhere')
184 if not os.path.isdir(s):
185 raise ArgTypeError('should be a directory', '/path/to/somewhere')
186
187 # Normalize, trim trailing path separators.
188 if len(s) > 1 and s[-1] == os.path.sep:
189 s = s[:-1]
190 return s
191
192
193def _collect_bisect_result_values(values, line):
194 """Collect bisect result values from output line.
195
196 Args:
197 values: Collected values are appending to this list.
198 line: One line of output string.
199 """
200 m = re.match(r'^BISECT_RESULT_VALUES=(.+)', line)
201 if m:
202 try:
203 values.extend(map(float, m.group(1).split()))
204 except ValueError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800205 raise errors.InternalError(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800206 'BISECT_RESULT_VALUES should be list of floats: %r' % m.group(1))
207
208
Kuang-che Wu88518882017-09-22 16:57:25 +0800209def check_executable(program):
210 """Checks whether a program is executable.
211
212 Args:
213 program: program path in question
214
215 Returns:
216 string as error message if `program` is not executable, or None otherwise.
217 It will return None if unable to determine as well.
218 """
219 returncode = util.call('which', program)
220 if returncode == 127: # No 'which' on this platform, skip the check.
221 return None
222 if returncode == 0: # is executable
223 return None
224
225 hint = ''
226 if not os.path.exists(program):
227 hint = 'Not in PATH?'
228 elif not os.path.isfile(program):
229 hint = 'Not a file'
230 elif not os.access(program, os.X_OK):
231 hint = 'Forgot to chmod +x?'
232 elif '/' not in program:
233 hint = 'Forgot to prepend "./" ?'
234 return '%r is not executable. %s' % (program, hint)
235
236
Kuang-che Wubc7bfce2019-05-21 18:43:16 +0800237def lookup_signal_name(signum):
238 """Look up signal name by signal number.
239
240 Args:
241 signum: signal number
242
243 Returns:
244 signal name, like "SIGTERM". "Unknown" for unexpected number.
245 """
246 for k, v in vars(signal).items():
247 if k.startswith('SIG') and '_' not in k and v == signum:
248 return k
249 return 'Unknown'
250
251
Kuang-che Wu443633f2019-02-27 00:58:33 +0800252def format_returncode(returncode):
Kuang-che Wubc7bfce2019-05-21 18:43:16 +0800253 # returncode is negative if the process is terminated by signal directly.
Kuang-che Wu443633f2019-02-27 00:58:33 +0800254 if returncode < 0:
255 signum = -returncode
Kuang-che Wubc7bfce2019-05-21 18:43:16 +0800256 signame = lookup_signal_name(signum)
Kuang-che Wu443633f2019-02-27 00:58:33 +0800257 return 'terminated by signal %d (%s)' % (signum, signame)
Kuang-che Wubc7bfce2019-05-21 18:43:16 +0800258 # Some programs, e.g. shell, handled signal X and exited with (128 + X).
259 if returncode > 128:
260 signum = returncode - 128
261 signame = lookup_signal_name(signum)
262 return 'exited with code %d; may be signal %d (%s)' % (returncode, signum,
263 signame)
Kuang-che Wu443633f2019-02-27 00:58:33 +0800264
265 return 'exited with code %d' % returncode
266
267
Kuang-che Wu88875db2017-07-20 10:47:53 +0800268def do_evaluate(evaluate_cmd, domain, rev):
269 """Invokes evaluator command.
270
271 The `evaluate_cmd` can get the target revision from the environment variable
272 named 'BISECT_REV'.
273
274 The result is determined according to the exit code of evaluator:
275 0: 'old'
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800276 1..124, 126, 127: 'new'
Kuang-che Wu88875db2017-07-20 10:47:53 +0800277 125: 'skip'
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800278 128..255: fatal error
Kuang-che Wu88875db2017-07-20 10:47:53 +0800279 terminated by signal: fatal error
280
281 p.s. the definition of result is compatible with git-bisect(1).
282
283 It also extracts additional values from evaluate_cmd's stdout lines which
284 match the following format:
285 BISECT_RESULT_VALUES=<float>[, <float>]*
286
287 Args:
288 evaluate_cmd: evaluator command.
289 domain: a bisect_kit.core.Domain instance.
290 rev: version to evaluate.
291
292 Returns:
293 (result, values):
294 result is one of 'old', 'new', 'skip'.
295 values are additional collected values, like performance score.
296
297 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800298 errors.ExecutionFatalError if evaluator returned fatal error code.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800299 """
300 env = os.environ.copy()
301 env['BISECT_REV'] = rev
302 domain.setenv(env, rev)
303
304 values = []
305 p = util.Popen(
306 evaluate_cmd,
307 env=env,
308 stdout_callback=lambda line: _collect_bisect_result_values(values, line))
309 returncode = p.wait()
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800310 if returncode < 0 or returncode >= 128:
Kuang-che Wu443633f2019-02-27 00:58:33 +0800311 raise errors.ExecutionFatalError(
312 'eval failed: %s' % format_returncode(returncode))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800313
314 if returncode == 0:
315 return 'old', values
316 if returncode == 125:
317 return 'skip', values
318 return 'new', values
319
320
321def do_switch(switch_cmd, domain, rev):
322 """Invokes switcher command.
323
324 The `switch_cmd` can get the target revision from the environment variable
325 named 'BISECT_REV'.
326
327 The result is determined according to the exit code of switcher:
328 0: switch succeeded
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800329 1..127: 'skip'
330 128..255: fatal error
Kuang-che Wu88875db2017-07-20 10:47:53 +0800331 terminated by signal: fatal error
332
333 In other words, any non-fatal errors are considered as 'skip'.
334
335 Args:
336 switch_cmd: switcher command.
337 domain: a bisect_kit.core.Domain instance.
338 rev: version to switch.
339
340 Returns:
341 None if switch successfully, 'skip' otherwise.
342
343 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800344 errors.ExecutionFatalError if switcher returned fatal error code.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800345 """
346 env = os.environ.copy()
347 env['BISECT_REV'] = rev
348 domain.setenv(env, rev)
349
350 returncode = util.call(*switch_cmd, env=env)
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800351 if returncode < 0 or returncode >= 128:
Kuang-che Wu443633f2019-02-27 00:58:33 +0800352 raise errors.ExecutionFatalError(
353 'switch failed: %s' % format_returncode(returncode))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800354
355 if returncode != 0:
356 return 'skip'
357 return None
358
359
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800360class BisectorCommandLine(object):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800361 """Bisector command line interface.
362
363 The typical usage pattern:
364
365 if __name__ == '__main__':
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800366 BisectorCommandLine(CustomDomain).main()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800367
368 where CustomDomain is a derived class of core.BisectDomain. See
Kuang-che Wu02170592018-07-09 21:42:44 +0800369 bisect_list.py as example.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800370
371 If you need to control the bisector using python code, the easier way is
372 passing command line arguments to main() function. For example,
373 bisector = Bisector(CustomDomain)
374 bisector.main('init', '--old', '123', '--new', '456')
375 bisector.main('config', 'switch', 'true')
376 bisector.main('config', 'eval', 'true')
377 bisector.main('run')
378 """
379
380 def __init__(self, domain_cls):
381 self.domain_cls = domain_cls
382 self.domain = None
383 self.states = None
384 self.strategy = None
385
386 @property
387 def config(self):
388 return self.states.config
389
Kuang-che Wu1dc5bd72019-01-19 00:14:46 +0800390 def _format_status(self, status):
391 if status in ('old', 'new'):
Kuang-che Wub6756d42019-01-25 12:19:55 +0800392 return '%s behavior' % status
Kuang-che Wu1dc5bd72019-01-19 00:14:46 +0800393 return status
394
Kuang-che Wu88875db2017-07-20 10:47:53 +0800395 def cmd_reset(self, _opts):
396 """Resets bisect session and clean up saved result."""
397 self.states.reset()
398
399 def cmd_init(self, opts):
400 """Initializes bisect session.
401
402 See init command's help message for more detail.
403 """
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800404 if (opts.old_value is None) != (opts.new_value is None):
405 raise errors.ArgumentError('--old_value and --new_value',
406 'both should be specified')
407 if opts.old_value is not None and opts.old_value == opts.new_value:
408 raise errors.ArgumentError('--old_value and --new_value',
409 'their values should be different')
410 if opts.recompute_init_values and opts.old_value is None:
411 raise errors.ArgumentError(
412 '--recompute_init_values',
413 '--old_value and --new_value must be specified '
414 'when --recompute_init_values is present')
415
Kuang-che Wu88875db2017-07-20 10:47:53 +0800416 config, revlist = self.domain_cls.init(opts)
Kuang-che Wu42551dd2018-01-16 17:27:20 +0800417 logger.info('found %d revs to bisect', len(revlist))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800418 logger.debug('revlist %r', revlist)
419 if 'new' not in config:
420 config['new'] = opts.new
421 if 'old' not in config:
422 config['old'] = opts.old
423 assert len(revlist) >= 2
424 assert config['new'] in revlist
425 assert config['old'] in revlist
426 old_idx = revlist.index(config['old'])
427 new_idx = revlist.index(config['new'])
428 assert old_idx < new_idx
429
Kuang-che Wu81cde452019-04-08 16:56:51 +0800430 config.update(
431 confidence=opts.confidence,
432 noisy=opts.noisy,
433 old_value=opts.old_value,
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800434 new_value=opts.new_value,
435 recompute_init_values=opts.recompute_init_values)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800436
437 self.states.init(config, revlist)
438 self.states.save()
439
440 def _switch_and_eval(self, rev, prev_rev=None):
441 """Switches and evaluates given version.
442
443 If current version equals to target, switch step will be skip.
444
445 Args:
446 rev: Target version.
447 prev_rev: Previous version.
448
449 Returns:
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800450 (step, sample):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800451 step: Last step executed ('switch' or 'eval').
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800452 sample (dict): sampling result of `rev`. The dict contains:
453 status: Execution result ('old', 'new', or 'skip').
454 values: For eval bisection, collected values from eval step.
455 switch_time: how much time in switch step
456 eval_time: how much time in eval step
Kuang-che Wu88875db2017-07-20 10:47:53 +0800457 """
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800458 sample = {'rev': rev}
Kuang-che Wu88875db2017-07-20 10:47:53 +0800459 if prev_rev != rev:
460 logger.debug('switch to rev=%s', rev)
461 t0 = time.time()
462 status = do_switch(self.config['switch'], self.domain, rev)
463 t1 = time.time()
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800464 sample['switch_time'] = t1 - t0
465 sample['status'] = status
Kuang-che Wu88875db2017-07-20 10:47:53 +0800466 if status == 'skip':
467 logger.debug('switch failed => skip')
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800468 return 'switch', sample
Kuang-che Wu88875db2017-07-20 10:47:53 +0800469
470 logger.debug('eval rev=%s', rev)
471 t0 = time.time()
472 status, values = do_evaluate(self.config['eval'], self.domain, rev)
473 t1 = time.time()
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800474 sample['eval_time'] = t1 - t0
475 sample['status'] = status
Kuang-che Wu88875db2017-07-20 10:47:53 +0800476 if status == 'skip':
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800477 return 'eval', sample
Kuang-che Wu81cde452019-04-08 16:56:51 +0800478
479 if self.strategy.is_value_bisection():
Kuang-che Wuc986b1d2019-04-15 16:45:20 +0800480 if not values:
481 raise errors.ExecutionFatalError(
482 'eval command (%s) terminated normally but did not output values' %
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800483 self.config['eval'])
484 sample['values'] = values
485 sample['status'] = self.strategy.classify_result_from_values(values)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800486
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800487 return 'eval', sample
Kuang-che Wu88875db2017-07-20 10:47:53 +0800488
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800489 def _next_idx_iter(self, opts, force):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800490 if opts.revs:
491 for rev in opts.revs:
492 idx = self.states.rev2idx(rev)
493 logger.info('try idx=%d rev=%s (command line specified)', idx, rev)
494 yield idx, rev
495 if opts.once:
496 break
497 else:
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800498 while force or not self.strategy.is_done():
Kuang-che Wu88875db2017-07-20 10:47:53 +0800499 idx = self.strategy.next_idx()
500 rev = self.states.idx2rev(idx)
501 logger.info('try idx=%d rev=%s', idx, rev)
502 yield idx, rev
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800503 force = False
Kuang-che Wu88875db2017-07-20 10:47:53 +0800504 if opts.once:
505 break
506
507 def cmd_run(self, opts):
508 """Performs bisection.
509
510 See run command's help message for more detail.
511
512 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800513 errors.VerificationFailed: The bisection range is verified false. We
Kuang-che Wu88875db2017-07-20 10:47:53 +0800514 expect 'old' at the first rev and 'new' at last rev.
Kuang-che Wue121fae2018-11-09 16:18:39 +0800515 errors.UnableToProceed: Too many errors to narrow down further the
516 bisection range.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800517 """
Kuang-che Wu8b654092018-11-09 17:56:25 +0800518 # Set dummy values in case exception raised before loop.
519 idx, rev = -1, None
520 try:
521 assert self.config.get('switch')
522 assert self.config.get('eval')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800523
Kuang-che Wu8b654092018-11-09 17:56:25 +0800524 prev_rev = None
525 force = opts.force
526 for idx, rev in self._next_idx_iter(opts, force):
527 if not force:
528 # Bail out if bisection range is unlikely true in order to prevent
529 # wasting time. This is necessary because some configurations (say,
530 # confidence) may be changed before cmd_run() and thus the bisection
531 # range becomes not acceptable.
532 self.strategy.check_verification_range()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800533
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800534 step, sample = self._switch_and_eval(rev, prev_rev=prev_rev)
535 self.states.add_history('sample', **sample)
Kuang-che Wu8b654092018-11-09 17:56:25 +0800536 self.states.save()
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800537 if 'values' in sample:
538 logger.info('rev=%s status => %s: %s', rev,
539 self._format_status(sample['status']), sample['values'])
540 else:
541 logger.info('rev=%s status => %s', rev,
542 self._format_status(sample['status']))
543 force = False
Kuang-che Wu8b654092018-11-09 17:56:25 +0800544
545 # Bail out if bisection range is unlikely true.
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800546 self.strategy.check_verification_range()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800547
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800548 self.strategy.add_sample(idx, **sample)
Kuang-che Wu8b654092018-11-09 17:56:25 +0800549 self.strategy.show_summary()
550
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800551 if step == 'switch' and sample['status'] == 'skip':
Kuang-che Wu8b654092018-11-09 17:56:25 +0800552 # Previous switch failed and thus the current version is unknown. Set
553 # it None, so next switch operation won't be bypassed (due to
554 # optimization).
555 prev_rev = None
556 else:
557 prev_rev = rev
558
559 logger.info('done')
560 old_idx, new_idx = self.strategy.get_range()
561 self.states.add_history('done')
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800562 self.states.save()
Kuang-che Wu8b654092018-11-09 17:56:25 +0800563 except Exception as e:
564 exception_name = e.__class__.__name__
565 self.states.add_history(
566 'failed', text='%s: %s' % (exception_name, e), index=idx, rev=rev)
567 self.states.save()
568 raise
569 finally:
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800570 if rev and self.strategy.state == self.strategy.STARTED:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800571 # progress so far
572 old_idx, new_idx = self.strategy.get_range()
573 self.states.add_history(
574 'range',
575 old=self.states.idx2rev(old_idx),
576 new=self.states.idx2rev(new_idx))
577 self.states.save()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800578
579 def cmd_view(self, opts):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800580 """Shows remaining candidates."""
Kuang-che Wue80bb872018-11-15 19:45:25 +0800581 old_idx, new_idx = self.strategy.get_range()
582 old, new = map(self.states.idx2rev, [old_idx, new_idx])
583 highlight_old_idx, highlight_new_idx = self.strategy.get_range(
584 self.strategy.confidence / 10.0)
585 summary = {
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800586 'rev_info': [info.to_dict() for info in self.strategy.rev_info],
Kuang-che Wue80bb872018-11-15 19:45:25 +0800587 'current_range': (old, new),
588 'highlight_range':
589 map(self.states.idx2rev, [highlight_old_idx, highlight_new_idx]),
590 'prob':
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800591 self.strategy.get_prob(),
Kuang-che Wue80bb872018-11-15 19:45:25 +0800592 'remaining_steps':
593 self.strategy.remaining_steps(),
594 }
595
596 if opts.verbose or opts.json:
597 interesting_indexes = set(range(len(summary['rev_info'])))
598 else:
599 interesting_indexes = set([old_idx, new_idx])
Kuang-che Wu15874b62019-01-11 21:10:27 +0800600 if self.strategy.prob:
601 for i, p in enumerate(self.strategy.prob):
602 if p > 0.05:
603 interesting_indexes.add(i)
Kuang-che Wue80bb872018-11-15 19:45:25 +0800604
605 self.domain.fill_candidate_summary(summary, interesting_indexes)
606
607 if opts.json:
608 print(json.dumps(summary, indent=2, sort_keys=True))
609 else:
610 self.show_summary(summary, interesting_indexes, verbose=opts.verbose)
611
612 def show_summary(self, summary, interesting_indexes, verbose=False):
613 old, new = summary['current_range']
614 old_idx, new_idx = map(self.states.data['revlist'].index, [old, new])
615
Kuang-che Wuaccf9202019-01-04 15:40:42 +0800616 for link in summary.get('links', []):
617 print('%s: %s' % (link['name'], link['url']))
618 if 'note' in link:
619 print(link['note'])
Kuang-che Wue80bb872018-11-15 19:45:25 +0800620
621 print('Range: (%s, %s], %s revs left' % (old, new, (new_idx - old_idx)))
Kuang-che Wua8c987f2019-01-18 14:26:43 +0800622 if summary.get('remaining_steps'):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800623 print('(roughly %d steps)' % summary['remaining_steps'])
624
625 for i, rev_info in enumerate(summary['rev_info']):
626 if (not verbose and not old_idx <= i <= new_idx and
627 not rev_info['result_counter']):
628 continue
629
630 detail = []
Kuang-che Wu05e416e2019-02-21 12:33:52 +0800631 if self.strategy.is_noisy() and summary['prob']:
Kuang-che Wua8c987f2019-01-18 14:26:43 +0800632 detail.append('%.4f%%' % (summary['prob'][i] * 100))
Kuang-che Wue80bb872018-11-15 19:45:25 +0800633 if rev_info['result_counter']:
634 detail.append(str(rev_info['result_counter']))
635 values = sorted(rev_info['values'])
636 if len(values) == 1:
637 detail.append('%.3f' % values[0])
638 elif len(values) > 1:
639 detail.append('n=%d,avg=%.3f,median=%.3f,min=%.3f,max=%.3f' %
640 (len(values), sum(values) / len(values),
641 values[len(values) // 2], values[0], values[-1]))
642
643 print('[%d] %s\t%s' % (i, rev_info['rev'], ' '.join(detail)))
644 if i in interesting_indexes:
645 if 'comment' in rev_info:
646 print('\t%s' % rev_info['comment'])
647 for action in rev_info.get('actions', []):
648 if 'text' in action:
649 print('\t%s' % action['text'])
650 if 'link' in action:
651 print('\t%s' % action['link'])
Kuang-che Wu88875db2017-07-20 10:47:53 +0800652
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800653 def _strategy_factory(self):
654 rev_info = self.states.load_rev_info()
655 assert rev_info
656 return strategy.NoisyBinarySearch(
657 rev_info,
658 self.states.rev2idx(self.config['old']),
659 self.states.rev2idx(self.config['new']),
660 old_value=self.config['old_value'],
661 new_value=self.config['new_value'],
662 recompute_init_values=self.config['recompute_init_values'],
663 confidence=self.config['confidence'],
664 observation=self.config['noisy'])
665
Kuang-che Wu88875db2017-07-20 10:47:53 +0800666 def current_status(self, session=None, session_base=None):
667 """Gets current bisect status.
668
669 Returns:
670 A dict describing current status. It contains following items:
671 inited: True iff the session file is initialized (init command has been
672 invoked). If not, below items are omitted.
673 old: Start of current estimated range.
674 new: End of current estimated range.
Kuang-che Wu8b654092018-11-09 17:56:25 +0800675 verified: The bisect range is already verified.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800676 estimated_noise: New estimated noise.
677 done: True if bisection is done, otherwise False.
678 """
679 self._create_states(session=session, session_base=session_base)
680 if self.states.load():
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800681 self.strategy = self._strategy_factory()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800682 left, right = self.strategy.get_range()
683 estimated_noise = self.strategy.get_noise_observation()
684
685 result = dict(
686 inited=True,
687 old=self.states.idx2rev(left),
688 new=self.states.idx2rev(right),
Kuang-che Wu8b654092018-11-09 17:56:25 +0800689 verified=self.strategy.is_range_verified(),
Kuang-che Wudd7f6f02018-06-28 18:19:30 +0800690 estimated_noise=estimated_noise,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800691 done=self.strategy.is_done())
692 else:
693 result = dict(inited=False)
694 return result
695
Kuang-che Wu8b654092018-11-09 17:56:25 +0800696 def cmd_log(self, opts):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800697 """Prints what has been done so far."""
Kuang-che Wu8b654092018-11-09 17:56:25 +0800698 history = []
Kuang-che Wu88875db2017-07-20 10:47:53 +0800699 for entry in self.states.data['history']:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800700 if opts.before and entry['timestamp'] >= opts.before:
701 continue
702 if opts.after and entry['timestamp'] <= opts.after:
703 continue
704 history.append(entry)
705
706 if opts.json:
707 print(json.dumps(history, indent=2))
708 return
709
710 for entry in history:
711 entry_time = datetime.datetime.fromtimestamp(int(entry['timestamp']))
712 if entry.get('event', 'sample') == 'sample':
713 print('{datetime} {rev} {status} {values} {comment}'.format(
714 datetime=entry_time,
715 rev=entry['rev'],
716 status=entry['status'] + ('*%d' % entry['times']
717 if entry.get('times', 1) > 1 else ''),
718 values=entry.get('values', ''),
719 comment=entry.get('comment', '')))
720 else:
721 print('%s %r' % (entry_time, entry))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800722
723 def cmd_next(self, _opts):
724 """Prints next suggested rev to bisect."""
Kuang-che Wu88875db2017-07-20 10:47:53 +0800725 if self.strategy.is_done():
726 print('done')
727 return
728
729 idx = self.strategy.next_idx()
730 rev = self.states.idx2rev(idx)
731 print(rev)
732
733 def cmd_switch(self, opts):
734 """Switches to given rev without eval."""
735 assert self.config.get('switch')
736
Kuang-che Wu88875db2017-07-20 10:47:53 +0800737 if opts.rev == 'next':
738 idx = self.strategy.next_idx()
739 rev = self.states.idx2rev(idx)
740 else:
Kuang-che Wu752228c2018-09-05 13:54:22 +0800741 rev = self.domain_cls.intra_revtype(opts.rev)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800742 assert rev
743
744 logger.info('switch to %s', rev)
745 status = do_switch(self.config['switch'], self.domain, rev)
746 if status:
747 print('switch failed')
748
749 def _add_revs_status_helper(self, revs, status):
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800750 if self.strategy.is_value_bisection():
751 assert status not in ('old', 'new')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800752 for rev, times in revs:
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800753 idx = self.states.rev2idx(rev)
754 sample = {'rev': rev, 'status': status}
755 # times=1 is default in the loader. Add 'times' entry only if necessary
756 # in order to simplify the dict.
757 if times > 1:
758 sample['times'] = times
759 self.states.add_history('sample', **sample)
760 self.states.save()
761 self.strategy.add_sample(idx, **sample)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800762
763 def cmd_new(self, opts):
764 """Tells bisect engine the said revs have "new" behavior."""
765 logger.info('set [%s] as new', opts.revs)
766 self._add_revs_status_helper(opts.revs, 'new')
767
768 def cmd_old(self, opts):
769 """Tells bisect engine the said revs have "old" behavior."""
770 logger.info('set [%s] as old', opts.revs)
771 self._add_revs_status_helper(opts.revs, 'old')
772
773 def cmd_skip(self, opts):
774 """Tells bisect engine the said revs have "skip" behavior."""
775 logger.info('set [%s] as skip', opts.revs)
776 self._add_revs_status_helper(opts.revs, 'skip')
777
778 def _create_states(self, session=None, session_base=None):
779 if not session:
780 session = DEFAULT_SESSION_NAME
781 if not session_base:
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800782 session_base = configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800783
784 session_file = os.path.join(session_base, session, self.domain_cls.__name__)
785
786 if self.states:
787 assert self.states.session_file == session_file
788 else:
Kuang-che Wuc5781932018-10-05 00:30:19 +0800789 self.states = core.BisectStates(session_file)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800790
791 def cmd_config(self, opts):
792 """Configures additional setting.
793
794 See config command's help message for more detail.
795 """
796 self.states.load()
797 self.domain = self.domain_cls(self.states.config)
798 if not opts.value:
799 print(self.states.config[opts.key])
800 return
801
802 if opts.key in ['switch', 'eval']:
Kuang-che Wu88518882017-09-22 16:57:25 +0800803 result = check_executable(opts.value[0])
804 if result:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800805 raise errors.ArgumentError('%s command' % opts.key, result)
Kuang-che Wu88518882017-09-22 16:57:25 +0800806
Kuang-che Wu88875db2017-07-20 10:47:53 +0800807 self.states.config[opts.key] = opts.value
808
809 elif opts.key == 'confidence':
810 if len(opts.value) != 1:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800811 raise errors.ArgumentError(
812 'confidence value',
813 'expected 1 value, %d values given' % len(opts.value))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800814 try:
815 self.states.config[opts.key] = float(opts.value[0])
816 except ValueError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800817 raise errors.ArgumentError('confidence value',
818 'invalid float value: %r' % opts.value[0])
Kuang-che Wu88875db2017-07-20 10:47:53 +0800819
820 elif opts.key == 'noisy':
821 if len(opts.value) != 1:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800822 raise errors.ArgumentError(
823 'noisy value',
824 'expected 1 value, %d values given' % len(opts.value))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800825 self.states.config[opts.key] = opts.value[0]
826
827 else:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800828 # unreachable
829 assert 0
Kuang-che Wu88875db2017-07-20 10:47:53 +0800830
831 self.states.save()
832
833 def create_argument_parser(self, prog):
Kuang-che Wub2376262017-11-20 18:05:24 +0800834 if self.domain_cls.help:
835 description = self.domain_cls.help
836 else:
837 description = 'Bisector for %s' % self.domain_cls.__name__
838 description += textwrap.dedent('''
839 When running switcher and evaluator, it will set BISECT_REV environment
840 variable, indicates current rev to switch/evaluate.
841 ''')
842
Kuang-che Wu88875db2017-07-20 10:47:53 +0800843 parser = argparse.ArgumentParser(
844 prog=prog,
845 formatter_class=argparse.RawDescriptionHelpFormatter,
Kuang-che Wub2376262017-11-20 18:05:24 +0800846 description=description)
Kuang-che Wu385279d2017-09-27 14:48:28 +0800847 common.add_common_arguments(parser)
848 parser.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800849 '--session_base',
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800850 default=configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE),
Kuang-che Wu88875db2017-07-20 10:47:53 +0800851 help='Directory to store sessions (default: %(default)r)')
852 parser.add_argument(
853 '--session',
854 default=DEFAULT_SESSION_NAME,
855 help='Session name (default: %(default)r)')
856 subparsers = parser.add_subparsers(
857 dest='command', title='commands', metavar='<command>')
858
859 parser_reset = subparsers.add_parser(
860 'reset', help='Reset bisect session and clean up saved result')
861 parser_reset.set_defaults(func=self.cmd_reset)
862
863 parser_init = subparsers.add_parser(
864 'init',
865 help='Initializes bisect session',
866 formatter_class=argparse.RawDescriptionHelpFormatter,
867 description=textwrap.dedent('''
868 Besides arguments for 'init' command, you also need to set 'switch'
869 and 'eval' command line via 'config' command.
870 $ bisector config switch <switch command and arguments>
871 $ bisector config eval <eval command and arguments>
872
873 The value of --noisy and --confidence could be changed by 'config'
874 command after 'init' as well.
875 '''))
876 parser_init.add_argument(
877 '--old',
878 required=True,
879 type=self.domain_cls.revtype,
880 help='Start of bisect range, which has old behavior')
881 parser_init.add_argument(
882 '--new',
883 required=True,
884 type=self.domain_cls.revtype,
885 help='End of bisect range, which has new behavior')
886 parser_init.add_argument(
887 '--noisy',
888 help='Enable noisy binary search and specify prior result. '
889 'For example, "old=1/10,new=2/3" means old fail rate is 1/10 '
890 'and new fail rate increased to 2/3. '
891 'Skip if not flaky, say, "new=2/3" means old is always good.')
892 parser_init.add_argument(
Kuang-che Wu81cde452019-04-08 16:56:51 +0800893 '--old_value',
894 type=float,
895 help='For performance test, value of old behavior')
896 parser_init.add_argument(
897 '--new_value',
898 type=float,
899 help='For performance test, value of new behavior')
900 parser_init.add_argument(
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800901 '--recompute_init_values',
902 action='store_true',
903 help='For performance test, recompute initial values')
904 parser_init.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800905 '--confidence',
906 type=float,
907 default=DEFAULT_CONFIDENCE,
908 help='Confidence level (default: %(default)r)')
909 parser_init.set_defaults(func=self.cmd_init)
910 self.domain_cls.add_init_arguments(parser_init)
911
912 parser_config = subparsers.add_parser(
913 'config', help='Configures additional setting')
914 parser_config.add_argument(
915 'key',
916 choices=['switch', 'eval', 'confidence', 'noisy'],
917 metavar='key',
918 help='What config to change. choices=[%(choices)s]')
919 parser_config.add_argument(
920 'value', nargs=argparse.REMAINDER, help='New value')
921 parser_config.set_defaults(func=self.cmd_config)
922
923 parser_run = subparsers.add_parser(
924 'run',
925 help='Performs bisection',
926 formatter_class=argparse.RawDescriptionHelpFormatter,
927 description=textwrap.dedent('''
928 This command does switch and eval to determine candidates having old or
929 new behavior.
930
931 By default, it attempts to try versions in binary search manner until
932 found the first version having new behavior.
933
934 If version numbers are specified on command line, it just tries those
935 versions and record the result.
936
937 Example:
938 Bisect automatically.
939 $ %(prog)s
940
941 Switch and run version "2.13" and "2.14" and then stop.
942 $ %(prog)s 2.13 2.14
943 '''))
944 parser_run.add_argument(
945 '-1', '--once', action='store_true', help='Only run one step')
946 parser_run.add_argument(
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800947 '--force',
948 action='store_true',
949 help="Run at least once even it's already done")
950 parser_run.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800951 'revs',
952 nargs='*',
Kuang-che Wu752228c2018-09-05 13:54:22 +0800953 type=self.domain_cls.intra_revtype,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800954 help='revs to switch+eval; '
955 'default is calculating automatically and run until done')
956 parser_run.set_defaults(func=self.cmd_run)
957
958 parser_switch = subparsers.add_parser(
959 'switch', help='Switch to given rev without eval')
960 parser_switch.add_argument(
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800961 'rev',
962 type=argtype_multiplexer(self.domain_cls.intra_revtype,
963 argtype_re('next', 'next')))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800964 parser_switch.set_defaults(func=self.cmd_switch)
965
966 parser_old = subparsers.add_parser(
967 'old', help='Tells bisect engine the said revs have "old" behavior')
968 parser_old.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800969 'revs',
970 nargs='+',
971 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800972 parser_old.set_defaults(func=self.cmd_old)
973
974 parser_new = subparsers.add_parser(
975 'new', help='Tells bisect engine the said revs have "new" behavior')
976 parser_new.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800977 'revs',
978 nargs='+',
979 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800980 parser_new.set_defaults(func=self.cmd_new)
981
982 parser_skip = subparsers.add_parser(
983 'skip', help='Tells bisect engine the said revs have "skip" behavior')
984 parser_skip.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800985 'revs',
986 nargs='+',
987 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800988 parser_skip.set_defaults(func=self.cmd_skip)
989
990 parser_view = subparsers.add_parser(
991 'view', help='Shows current progress and candidates')
Kuang-che Wue80bb872018-11-15 19:45:25 +0800992 parser_view.add_argument('--verbose', '-v', action='store_true')
993 parser_view.add_argument('--json', action='store_true')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800994 parser_view.set_defaults(func=self.cmd_view)
995
996 parser_log = subparsers.add_parser(
997 'log', help='Prints what has been done so far')
Kuang-che Wu8b654092018-11-09 17:56:25 +0800998 parser_log.add_argument('--before', type=float)
999 parser_log.add_argument('--after', type=float)
1000 parser_log.add_argument(
1001 '--json', action='store_true', help='Machine readable output')
Kuang-che Wu88875db2017-07-20 10:47:53 +08001002 parser_log.set_defaults(func=self.cmd_log)
1003
1004 parser_next = subparsers.add_parser(
1005 'next', help='Prints next suggested rev to bisect')
1006 parser_next.set_defaults(func=self.cmd_next)
1007
1008 return parser
1009
1010 def main(self, *args, **kwargs):
1011 """Command line main function.
1012
1013 Args:
1014 *args: Command line arguments.
1015 **kwargs: additional non command line arguments passed by script code.
1016 {
1017 'prog': Program name; optional.
1018 }
1019 """
Kuang-che Wu385279d2017-09-27 14:48:28 +08001020 common.init()
Kuang-che Wu88875db2017-07-20 10:47:53 +08001021 parser = self.create_argument_parser(kwargs.get('prog'))
1022 opts = parser.parse_args(args or None)
1023 common.config_logging(opts)
1024
1025 self._create_states(session=opts.session, session_base=opts.session_base)
1026 if opts.command not in ('init', 'reset', 'config'):
1027 self.states.load()
1028 self.domain = self.domain_cls(self.states.config)
Kuang-che Wu4f6f9122019-04-23 17:44:46 +08001029 self.strategy = self._strategy_factory()
Kuang-che Wu88875db2017-07-20 10:47:53 +08001030
1031 return opts.func(opts)