blob: 6e3fc6eed613f7620a8ce9adde8fd83244894792 [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 Wu443633f2019-02-27 00:58:33 +0800237def format_returncode(returncode):
238 if returncode < 0:
239 signum = -returncode
240 signame = 'Unknown'
241 for k, v in vars(signal).items():
242 if k.startswith('SIG') and '_' not in k and v == signum:
243 signame = k
244 return 'terminated by signal %d (%s)' % (signum, signame)
245
246 return 'exited with code %d' % returncode
247
248
Kuang-che Wu88875db2017-07-20 10:47:53 +0800249def do_evaluate(evaluate_cmd, domain, rev):
250 """Invokes evaluator command.
251
252 The `evaluate_cmd` can get the target revision from the environment variable
253 named 'BISECT_REV'.
254
255 The result is determined according to the exit code of evaluator:
256 0: 'old'
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800257 1..124, 126, 127: 'new'
Kuang-che Wu88875db2017-07-20 10:47:53 +0800258 125: 'skip'
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800259 128..255: fatal error
Kuang-che Wu88875db2017-07-20 10:47:53 +0800260 terminated by signal: fatal error
261
262 p.s. the definition of result is compatible with git-bisect(1).
263
264 It also extracts additional values from evaluate_cmd's stdout lines which
265 match the following format:
266 BISECT_RESULT_VALUES=<float>[, <float>]*
267
268 Args:
269 evaluate_cmd: evaluator command.
270 domain: a bisect_kit.core.Domain instance.
271 rev: version to evaluate.
272
273 Returns:
274 (result, values):
275 result is one of 'old', 'new', 'skip'.
276 values are additional collected values, like performance score.
277
278 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800279 errors.ExecutionFatalError if evaluator returned fatal error code.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800280 """
281 env = os.environ.copy()
282 env['BISECT_REV'] = rev
283 domain.setenv(env, rev)
284
285 values = []
286 p = util.Popen(
287 evaluate_cmd,
288 env=env,
289 stdout_callback=lambda line: _collect_bisect_result_values(values, line))
290 returncode = p.wait()
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800291 if returncode < 0 or returncode >= 128:
Kuang-che Wu443633f2019-02-27 00:58:33 +0800292 raise errors.ExecutionFatalError(
293 'eval failed: %s' % format_returncode(returncode))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800294
295 if returncode == 0:
296 return 'old', values
297 if returncode == 125:
298 return 'skip', values
299 return 'new', values
300
301
302def do_switch(switch_cmd, domain, rev):
303 """Invokes switcher command.
304
305 The `switch_cmd` can get the target revision from the environment variable
306 named 'BISECT_REV'.
307
308 The result is determined according to the exit code of switcher:
309 0: switch succeeded
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800310 1..127: 'skip'
311 128..255: fatal error
Kuang-che Wu88875db2017-07-20 10:47:53 +0800312 terminated by signal: fatal error
313
314 In other words, any non-fatal errors are considered as 'skip'.
315
316 Args:
317 switch_cmd: switcher command.
318 domain: a bisect_kit.core.Domain instance.
319 rev: version to switch.
320
321 Returns:
322 None if switch successfully, 'skip' otherwise.
323
324 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800325 errors.ExecutionFatalError if switcher returned fatal error code.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800326 """
327 env = os.environ.copy()
328 env['BISECT_REV'] = rev
329 domain.setenv(env, rev)
330
331 returncode = util.call(*switch_cmd, env=env)
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800332 if returncode < 0 or returncode >= 128:
Kuang-che Wu443633f2019-02-27 00:58:33 +0800333 raise errors.ExecutionFatalError(
334 'switch failed: %s' % format_returncode(returncode))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800335
336 if returncode != 0:
337 return 'skip'
338 return None
339
340
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800341class BisectorCommandLine(object):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800342 """Bisector command line interface.
343
344 The typical usage pattern:
345
346 if __name__ == '__main__':
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800347 BisectorCommandLine(CustomDomain).main()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800348
349 where CustomDomain is a derived class of core.BisectDomain. See
Kuang-che Wu02170592018-07-09 21:42:44 +0800350 bisect_list.py as example.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800351
352 If you need to control the bisector using python code, the easier way is
353 passing command line arguments to main() function. For example,
354 bisector = Bisector(CustomDomain)
355 bisector.main('init', '--old', '123', '--new', '456')
356 bisector.main('config', 'switch', 'true')
357 bisector.main('config', 'eval', 'true')
358 bisector.main('run')
359 """
360
361 def __init__(self, domain_cls):
362 self.domain_cls = domain_cls
363 self.domain = None
364 self.states = None
365 self.strategy = None
366
367 @property
368 def config(self):
369 return self.states.config
370
Kuang-che Wu1dc5bd72019-01-19 00:14:46 +0800371 def _format_status(self, status):
372 if status in ('old', 'new'):
Kuang-che Wub6756d42019-01-25 12:19:55 +0800373 return '%s behavior' % status
Kuang-che Wu1dc5bd72019-01-19 00:14:46 +0800374 return status
375
Kuang-che Wu88875db2017-07-20 10:47:53 +0800376 def cmd_reset(self, _opts):
377 """Resets bisect session and clean up saved result."""
378 self.states.reset()
379
380 def cmd_init(self, opts):
381 """Initializes bisect session.
382
383 See init command's help message for more detail.
384 """
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800385 if (opts.old_value is None) != (opts.new_value is None):
386 raise errors.ArgumentError('--old_value and --new_value',
387 'both should be specified')
388 if opts.old_value is not None and opts.old_value == opts.new_value:
389 raise errors.ArgumentError('--old_value and --new_value',
390 'their values should be different')
391 if opts.recompute_init_values and opts.old_value is None:
392 raise errors.ArgumentError(
393 '--recompute_init_values',
394 '--old_value and --new_value must be specified '
395 'when --recompute_init_values is present')
396
Kuang-che Wu88875db2017-07-20 10:47:53 +0800397 config, revlist = self.domain_cls.init(opts)
Kuang-che Wu42551dd2018-01-16 17:27:20 +0800398 logger.info('found %d revs to bisect', len(revlist))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800399 logger.debug('revlist %r', revlist)
400 if 'new' not in config:
401 config['new'] = opts.new
402 if 'old' not in config:
403 config['old'] = opts.old
404 assert len(revlist) >= 2
405 assert config['new'] in revlist
406 assert config['old'] in revlist
407 old_idx = revlist.index(config['old'])
408 new_idx = revlist.index(config['new'])
409 assert old_idx < new_idx
410
Kuang-che Wu81cde452019-04-08 16:56:51 +0800411 config.update(
412 confidence=opts.confidence,
413 noisy=opts.noisy,
414 old_value=opts.old_value,
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800415 new_value=opts.new_value,
416 recompute_init_values=opts.recompute_init_values)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800417
418 self.states.init(config, revlist)
419 self.states.save()
420
421 def _switch_and_eval(self, rev, prev_rev=None):
422 """Switches and evaluates given version.
423
424 If current version equals to target, switch step will be skip.
425
426 Args:
427 rev: Target version.
428 prev_rev: Previous version.
429
430 Returns:
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800431 (step, sample):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800432 step: Last step executed ('switch' or 'eval').
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800433 sample (dict): sampling result of `rev`. The dict contains:
434 status: Execution result ('old', 'new', or 'skip').
435 values: For eval bisection, collected values from eval step.
436 switch_time: how much time in switch step
437 eval_time: how much time in eval step
Kuang-che Wu88875db2017-07-20 10:47:53 +0800438 """
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800439 sample = {'rev': rev}
Kuang-che Wu88875db2017-07-20 10:47:53 +0800440 if prev_rev != rev:
441 logger.debug('switch to rev=%s', rev)
442 t0 = time.time()
443 status = do_switch(self.config['switch'], self.domain, rev)
444 t1 = time.time()
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800445 sample['switch_time'] = t1 - t0
446 sample['status'] = status
Kuang-che Wu88875db2017-07-20 10:47:53 +0800447 if status == 'skip':
448 logger.debug('switch failed => skip')
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800449 return 'switch', sample
Kuang-che Wu88875db2017-07-20 10:47:53 +0800450
451 logger.debug('eval rev=%s', rev)
452 t0 = time.time()
453 status, values = do_evaluate(self.config['eval'], self.domain, rev)
454 t1 = time.time()
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800455 sample['eval_time'] = t1 - t0
456 sample['status'] = status
Kuang-che Wu88875db2017-07-20 10:47:53 +0800457 if status == 'skip':
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800458 return 'eval', sample
Kuang-che Wu81cde452019-04-08 16:56:51 +0800459
460 if self.strategy.is_value_bisection():
Kuang-che Wuc986b1d2019-04-15 16:45:20 +0800461 if not values:
462 raise errors.ExecutionFatalError(
463 'eval command (%s) terminated normally but did not output values' %
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800464 self.config['eval'])
465 sample['values'] = values
466 sample['status'] = self.strategy.classify_result_from_values(values)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800467
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800468 return 'eval', sample
Kuang-che Wu88875db2017-07-20 10:47:53 +0800469
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800470 def _next_idx_iter(self, opts, force):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800471 if opts.revs:
472 for rev in opts.revs:
473 idx = self.states.rev2idx(rev)
474 logger.info('try idx=%d rev=%s (command line specified)', idx, rev)
475 yield idx, rev
476 if opts.once:
477 break
478 else:
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800479 while force or not self.strategy.is_done():
Kuang-che Wu88875db2017-07-20 10:47:53 +0800480 idx = self.strategy.next_idx()
481 rev = self.states.idx2rev(idx)
482 logger.info('try idx=%d rev=%s', idx, rev)
483 yield idx, rev
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800484 force = False
Kuang-che Wu88875db2017-07-20 10:47:53 +0800485 if opts.once:
486 break
487
488 def cmd_run(self, opts):
489 """Performs bisection.
490
491 See run command's help message for more detail.
492
493 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800494 errors.VerificationFailed: The bisection range is verified false. We
Kuang-che Wu88875db2017-07-20 10:47:53 +0800495 expect 'old' at the first rev and 'new' at last rev.
Kuang-che Wue121fae2018-11-09 16:18:39 +0800496 errors.UnableToProceed: Too many errors to narrow down further the
497 bisection range.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800498 """
Kuang-che Wu8b654092018-11-09 17:56:25 +0800499 # Set dummy values in case exception raised before loop.
500 idx, rev = -1, None
501 try:
502 assert self.config.get('switch')
503 assert self.config.get('eval')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800504
Kuang-che Wu8b654092018-11-09 17:56:25 +0800505 prev_rev = None
506 force = opts.force
507 for idx, rev in self._next_idx_iter(opts, force):
508 if not force:
509 # Bail out if bisection range is unlikely true in order to prevent
510 # wasting time. This is necessary because some configurations (say,
511 # confidence) may be changed before cmd_run() and thus the bisection
512 # range becomes not acceptable.
513 self.strategy.check_verification_range()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800514
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800515 step, sample = self._switch_and_eval(rev, prev_rev=prev_rev)
516 self.states.add_history('sample', **sample)
Kuang-che Wu8b654092018-11-09 17:56:25 +0800517 self.states.save()
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800518 if 'values' in sample:
519 logger.info('rev=%s status => %s: %s', rev,
520 self._format_status(sample['status']), sample['values'])
521 else:
522 logger.info('rev=%s status => %s', rev,
523 self._format_status(sample['status']))
524 force = False
Kuang-che Wu8b654092018-11-09 17:56:25 +0800525
526 # Bail out if bisection range is unlikely true.
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800527 self.strategy.check_verification_range()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800528
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800529 self.strategy.add_sample(idx, **sample)
Kuang-che Wu8b654092018-11-09 17:56:25 +0800530 self.strategy.show_summary()
531
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800532 if step == 'switch' and sample['status'] == 'skip':
Kuang-che Wu8b654092018-11-09 17:56:25 +0800533 # Previous switch failed and thus the current version is unknown. Set
534 # it None, so next switch operation won't be bypassed (due to
535 # optimization).
536 prev_rev = None
537 else:
538 prev_rev = rev
539
540 logger.info('done')
541 old_idx, new_idx = self.strategy.get_range()
542 self.states.add_history('done')
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800543 self.states.save()
Kuang-che Wu8b654092018-11-09 17:56:25 +0800544 except Exception as e:
545 exception_name = e.__class__.__name__
546 self.states.add_history(
547 'failed', text='%s: %s' % (exception_name, e), index=idx, rev=rev)
548 self.states.save()
549 raise
550 finally:
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800551 if rev and self.strategy.state == self.strategy.STARTED:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800552 # progress so far
553 old_idx, new_idx = self.strategy.get_range()
554 self.states.add_history(
555 'range',
556 old=self.states.idx2rev(old_idx),
557 new=self.states.idx2rev(new_idx))
558 self.states.save()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800559
560 def cmd_view(self, opts):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800561 """Shows remaining candidates."""
Kuang-che Wue80bb872018-11-15 19:45:25 +0800562 old_idx, new_idx = self.strategy.get_range()
563 old, new = map(self.states.idx2rev, [old_idx, new_idx])
564 highlight_old_idx, highlight_new_idx = self.strategy.get_range(
565 self.strategy.confidence / 10.0)
566 summary = {
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800567 'rev_info': [info.to_dict() for info in self.strategy.rev_info],
Kuang-che Wue80bb872018-11-15 19:45:25 +0800568 'current_range': (old, new),
569 'highlight_range':
570 map(self.states.idx2rev, [highlight_old_idx, highlight_new_idx]),
571 'prob':
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800572 self.strategy.get_prob(),
Kuang-che Wue80bb872018-11-15 19:45:25 +0800573 'remaining_steps':
574 self.strategy.remaining_steps(),
575 }
576
577 if opts.verbose or opts.json:
578 interesting_indexes = set(range(len(summary['rev_info'])))
579 else:
580 interesting_indexes = set([old_idx, new_idx])
Kuang-che Wu15874b62019-01-11 21:10:27 +0800581 if self.strategy.prob:
582 for i, p in enumerate(self.strategy.prob):
583 if p > 0.05:
584 interesting_indexes.add(i)
Kuang-che Wue80bb872018-11-15 19:45:25 +0800585
586 self.domain.fill_candidate_summary(summary, interesting_indexes)
587
588 if opts.json:
589 print(json.dumps(summary, indent=2, sort_keys=True))
590 else:
591 self.show_summary(summary, interesting_indexes, verbose=opts.verbose)
592
593 def show_summary(self, summary, interesting_indexes, verbose=False):
594 old, new = summary['current_range']
595 old_idx, new_idx = map(self.states.data['revlist'].index, [old, new])
596
Kuang-che Wuaccf9202019-01-04 15:40:42 +0800597 for link in summary.get('links', []):
598 print('%s: %s' % (link['name'], link['url']))
599 if 'note' in link:
600 print(link['note'])
Kuang-che Wue80bb872018-11-15 19:45:25 +0800601
602 print('Range: (%s, %s], %s revs left' % (old, new, (new_idx - old_idx)))
Kuang-che Wua8c987f2019-01-18 14:26:43 +0800603 if summary.get('remaining_steps'):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800604 print('(roughly %d steps)' % summary['remaining_steps'])
605
606 for i, rev_info in enumerate(summary['rev_info']):
607 if (not verbose and not old_idx <= i <= new_idx and
608 not rev_info['result_counter']):
609 continue
610
611 detail = []
Kuang-che Wu05e416e2019-02-21 12:33:52 +0800612 if self.strategy.is_noisy() and summary['prob']:
Kuang-che Wua8c987f2019-01-18 14:26:43 +0800613 detail.append('%.4f%%' % (summary['prob'][i] * 100))
Kuang-che Wue80bb872018-11-15 19:45:25 +0800614 if rev_info['result_counter']:
615 detail.append(str(rev_info['result_counter']))
616 values = sorted(rev_info['values'])
617 if len(values) == 1:
618 detail.append('%.3f' % values[0])
619 elif len(values) > 1:
620 detail.append('n=%d,avg=%.3f,median=%.3f,min=%.3f,max=%.3f' %
621 (len(values), sum(values) / len(values),
622 values[len(values) // 2], values[0], values[-1]))
623
624 print('[%d] %s\t%s' % (i, rev_info['rev'], ' '.join(detail)))
625 if i in interesting_indexes:
626 if 'comment' in rev_info:
627 print('\t%s' % rev_info['comment'])
628 for action in rev_info.get('actions', []):
629 if 'text' in action:
630 print('\t%s' % action['text'])
631 if 'link' in action:
632 print('\t%s' % action['link'])
Kuang-che Wu88875db2017-07-20 10:47:53 +0800633
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800634 def _strategy_factory(self):
635 rev_info = self.states.load_rev_info()
636 assert rev_info
637 return strategy.NoisyBinarySearch(
638 rev_info,
639 self.states.rev2idx(self.config['old']),
640 self.states.rev2idx(self.config['new']),
641 old_value=self.config['old_value'],
642 new_value=self.config['new_value'],
643 recompute_init_values=self.config['recompute_init_values'],
644 confidence=self.config['confidence'],
645 observation=self.config['noisy'])
646
Kuang-che Wu88875db2017-07-20 10:47:53 +0800647 def current_status(self, session=None, session_base=None):
648 """Gets current bisect status.
649
650 Returns:
651 A dict describing current status. It contains following items:
652 inited: True iff the session file is initialized (init command has been
653 invoked). If not, below items are omitted.
654 old: Start of current estimated range.
655 new: End of current estimated range.
Kuang-che Wu8b654092018-11-09 17:56:25 +0800656 verified: The bisect range is already verified.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800657 estimated_noise: New estimated noise.
658 done: True if bisection is done, otherwise False.
659 """
660 self._create_states(session=session, session_base=session_base)
661 if self.states.load():
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800662 self.strategy = self._strategy_factory()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800663 left, right = self.strategy.get_range()
664 estimated_noise = self.strategy.get_noise_observation()
665
666 result = dict(
667 inited=True,
668 old=self.states.idx2rev(left),
669 new=self.states.idx2rev(right),
Kuang-che Wu8b654092018-11-09 17:56:25 +0800670 verified=self.strategy.is_range_verified(),
Kuang-che Wudd7f6f02018-06-28 18:19:30 +0800671 estimated_noise=estimated_noise,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800672 done=self.strategy.is_done())
673 else:
674 result = dict(inited=False)
675 return result
676
Kuang-che Wu8b654092018-11-09 17:56:25 +0800677 def cmd_log(self, opts):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800678 """Prints what has been done so far."""
Kuang-che Wu8b654092018-11-09 17:56:25 +0800679 history = []
Kuang-che Wu88875db2017-07-20 10:47:53 +0800680 for entry in self.states.data['history']:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800681 if opts.before and entry['timestamp'] >= opts.before:
682 continue
683 if opts.after and entry['timestamp'] <= opts.after:
684 continue
685 history.append(entry)
686
687 if opts.json:
688 print(json.dumps(history, indent=2))
689 return
690
691 for entry in history:
692 entry_time = datetime.datetime.fromtimestamp(int(entry['timestamp']))
693 if entry.get('event', 'sample') == 'sample':
694 print('{datetime} {rev} {status} {values} {comment}'.format(
695 datetime=entry_time,
696 rev=entry['rev'],
697 status=entry['status'] + ('*%d' % entry['times']
698 if entry.get('times', 1) > 1 else ''),
699 values=entry.get('values', ''),
700 comment=entry.get('comment', '')))
701 else:
702 print('%s %r' % (entry_time, entry))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800703
704 def cmd_next(self, _opts):
705 """Prints next suggested rev to bisect."""
Kuang-che Wu88875db2017-07-20 10:47:53 +0800706 if self.strategy.is_done():
707 print('done')
708 return
709
710 idx = self.strategy.next_idx()
711 rev = self.states.idx2rev(idx)
712 print(rev)
713
714 def cmd_switch(self, opts):
715 """Switches to given rev without eval."""
716 assert self.config.get('switch')
717
Kuang-che Wu88875db2017-07-20 10:47:53 +0800718 if opts.rev == 'next':
719 idx = self.strategy.next_idx()
720 rev = self.states.idx2rev(idx)
721 else:
Kuang-che Wu752228c2018-09-05 13:54:22 +0800722 rev = self.domain_cls.intra_revtype(opts.rev)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800723 assert rev
724
725 logger.info('switch to %s', rev)
726 status = do_switch(self.config['switch'], self.domain, rev)
727 if status:
728 print('switch failed')
729
730 def _add_revs_status_helper(self, revs, status):
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800731 if self.strategy.is_value_bisection():
732 assert status not in ('old', 'new')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800733 for rev, times in revs:
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800734 idx = self.states.rev2idx(rev)
735 sample = {'rev': rev, 'status': status}
736 # times=1 is default in the loader. Add 'times' entry only if necessary
737 # in order to simplify the dict.
738 if times > 1:
739 sample['times'] = times
740 self.states.add_history('sample', **sample)
741 self.states.save()
742 self.strategy.add_sample(idx, **sample)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800743
744 def cmd_new(self, opts):
745 """Tells bisect engine the said revs have "new" behavior."""
746 logger.info('set [%s] as new', opts.revs)
747 self._add_revs_status_helper(opts.revs, 'new')
748
749 def cmd_old(self, opts):
750 """Tells bisect engine the said revs have "old" behavior."""
751 logger.info('set [%s] as old', opts.revs)
752 self._add_revs_status_helper(opts.revs, 'old')
753
754 def cmd_skip(self, opts):
755 """Tells bisect engine the said revs have "skip" behavior."""
756 logger.info('set [%s] as skip', opts.revs)
757 self._add_revs_status_helper(opts.revs, 'skip')
758
759 def _create_states(self, session=None, session_base=None):
760 if not session:
761 session = DEFAULT_SESSION_NAME
762 if not session_base:
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800763 session_base = configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800764
765 session_file = os.path.join(session_base, session, self.domain_cls.__name__)
766
767 if self.states:
768 assert self.states.session_file == session_file
769 else:
Kuang-che Wuc5781932018-10-05 00:30:19 +0800770 self.states = core.BisectStates(session_file)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800771
772 def cmd_config(self, opts):
773 """Configures additional setting.
774
775 See config command's help message for more detail.
776 """
777 self.states.load()
778 self.domain = self.domain_cls(self.states.config)
779 if not opts.value:
780 print(self.states.config[opts.key])
781 return
782
783 if opts.key in ['switch', 'eval']:
Kuang-che Wu88518882017-09-22 16:57:25 +0800784 result = check_executable(opts.value[0])
785 if result:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800786 raise errors.ArgumentError('%s command' % opts.key, result)
Kuang-che Wu88518882017-09-22 16:57:25 +0800787
Kuang-che Wu88875db2017-07-20 10:47:53 +0800788 self.states.config[opts.key] = opts.value
789
790 elif opts.key == 'confidence':
791 if len(opts.value) != 1:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800792 raise errors.ArgumentError(
793 'confidence value',
794 'expected 1 value, %d values given' % len(opts.value))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800795 try:
796 self.states.config[opts.key] = float(opts.value[0])
797 except ValueError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800798 raise errors.ArgumentError('confidence value',
799 'invalid float value: %r' % opts.value[0])
Kuang-che Wu88875db2017-07-20 10:47:53 +0800800
801 elif opts.key == 'noisy':
802 if len(opts.value) != 1:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800803 raise errors.ArgumentError(
804 'noisy value',
805 'expected 1 value, %d values given' % len(opts.value))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800806 self.states.config[opts.key] = opts.value[0]
807
808 else:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800809 # unreachable
810 assert 0
Kuang-che Wu88875db2017-07-20 10:47:53 +0800811
812 self.states.save()
813
814 def create_argument_parser(self, prog):
Kuang-che Wub2376262017-11-20 18:05:24 +0800815 if self.domain_cls.help:
816 description = self.domain_cls.help
817 else:
818 description = 'Bisector for %s' % self.domain_cls.__name__
819 description += textwrap.dedent('''
820 When running switcher and evaluator, it will set BISECT_REV environment
821 variable, indicates current rev to switch/evaluate.
822 ''')
823
Kuang-che Wu88875db2017-07-20 10:47:53 +0800824 parser = argparse.ArgumentParser(
825 prog=prog,
826 formatter_class=argparse.RawDescriptionHelpFormatter,
Kuang-che Wub2376262017-11-20 18:05:24 +0800827 description=description)
Kuang-che Wu385279d2017-09-27 14:48:28 +0800828 common.add_common_arguments(parser)
829 parser.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800830 '--session_base',
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800831 default=configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE),
Kuang-che Wu88875db2017-07-20 10:47:53 +0800832 help='Directory to store sessions (default: %(default)r)')
833 parser.add_argument(
834 '--session',
835 default=DEFAULT_SESSION_NAME,
836 help='Session name (default: %(default)r)')
837 subparsers = parser.add_subparsers(
838 dest='command', title='commands', metavar='<command>')
839
840 parser_reset = subparsers.add_parser(
841 'reset', help='Reset bisect session and clean up saved result')
842 parser_reset.set_defaults(func=self.cmd_reset)
843
844 parser_init = subparsers.add_parser(
845 'init',
846 help='Initializes bisect session',
847 formatter_class=argparse.RawDescriptionHelpFormatter,
848 description=textwrap.dedent('''
849 Besides arguments for 'init' command, you also need to set 'switch'
850 and 'eval' command line via 'config' command.
851 $ bisector config switch <switch command and arguments>
852 $ bisector config eval <eval command and arguments>
853
854 The value of --noisy and --confidence could be changed by 'config'
855 command after 'init' as well.
856 '''))
857 parser_init.add_argument(
858 '--old',
859 required=True,
860 type=self.domain_cls.revtype,
861 help='Start of bisect range, which has old behavior')
862 parser_init.add_argument(
863 '--new',
864 required=True,
865 type=self.domain_cls.revtype,
866 help='End of bisect range, which has new behavior')
867 parser_init.add_argument(
868 '--noisy',
869 help='Enable noisy binary search and specify prior result. '
870 'For example, "old=1/10,new=2/3" means old fail rate is 1/10 '
871 'and new fail rate increased to 2/3. '
872 'Skip if not flaky, say, "new=2/3" means old is always good.')
873 parser_init.add_argument(
Kuang-che Wu81cde452019-04-08 16:56:51 +0800874 '--old_value',
875 type=float,
876 help='For performance test, value of old behavior')
877 parser_init.add_argument(
878 '--new_value',
879 type=float,
880 help='For performance test, value of new behavior')
881 parser_init.add_argument(
Kuang-che Wu4f6f9122019-04-23 17:44:46 +0800882 '--recompute_init_values',
883 action='store_true',
884 help='For performance test, recompute initial values')
885 parser_init.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800886 '--confidence',
887 type=float,
888 default=DEFAULT_CONFIDENCE,
889 help='Confidence level (default: %(default)r)')
890 parser_init.set_defaults(func=self.cmd_init)
891 self.domain_cls.add_init_arguments(parser_init)
892
893 parser_config = subparsers.add_parser(
894 'config', help='Configures additional setting')
895 parser_config.add_argument(
896 'key',
897 choices=['switch', 'eval', 'confidence', 'noisy'],
898 metavar='key',
899 help='What config to change. choices=[%(choices)s]')
900 parser_config.add_argument(
901 'value', nargs=argparse.REMAINDER, help='New value')
902 parser_config.set_defaults(func=self.cmd_config)
903
904 parser_run = subparsers.add_parser(
905 'run',
906 help='Performs bisection',
907 formatter_class=argparse.RawDescriptionHelpFormatter,
908 description=textwrap.dedent('''
909 This command does switch and eval to determine candidates having old or
910 new behavior.
911
912 By default, it attempts to try versions in binary search manner until
913 found the first version having new behavior.
914
915 If version numbers are specified on command line, it just tries those
916 versions and record the result.
917
918 Example:
919 Bisect automatically.
920 $ %(prog)s
921
922 Switch and run version "2.13" and "2.14" and then stop.
923 $ %(prog)s 2.13 2.14
924 '''))
925 parser_run.add_argument(
926 '-1', '--once', action='store_true', help='Only run one step')
927 parser_run.add_argument(
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800928 '--force',
929 action='store_true',
930 help="Run at least once even it's already done")
931 parser_run.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800932 'revs',
933 nargs='*',
Kuang-che Wu752228c2018-09-05 13:54:22 +0800934 type=self.domain_cls.intra_revtype,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800935 help='revs to switch+eval; '
936 'default is calculating automatically and run until done')
937 parser_run.set_defaults(func=self.cmd_run)
938
939 parser_switch = subparsers.add_parser(
940 'switch', help='Switch to given rev without eval')
941 parser_switch.add_argument(
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800942 'rev',
943 type=argtype_multiplexer(self.domain_cls.intra_revtype,
944 argtype_re('next', 'next')))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800945 parser_switch.set_defaults(func=self.cmd_switch)
946
947 parser_old = subparsers.add_parser(
948 'old', help='Tells bisect engine the said revs have "old" behavior')
949 parser_old.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800950 'revs',
951 nargs='+',
952 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800953 parser_old.set_defaults(func=self.cmd_old)
954
955 parser_new = subparsers.add_parser(
956 'new', help='Tells bisect engine the said revs have "new" behavior')
957 parser_new.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800958 'revs',
959 nargs='+',
960 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800961 parser_new.set_defaults(func=self.cmd_new)
962
963 parser_skip = subparsers.add_parser(
964 'skip', help='Tells bisect engine the said revs have "skip" behavior')
965 parser_skip.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800966 'revs',
967 nargs='+',
968 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800969 parser_skip.set_defaults(func=self.cmd_skip)
970
971 parser_view = subparsers.add_parser(
972 'view', help='Shows current progress and candidates')
Kuang-che Wue80bb872018-11-15 19:45:25 +0800973 parser_view.add_argument('--verbose', '-v', action='store_true')
974 parser_view.add_argument('--json', action='store_true')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800975 parser_view.set_defaults(func=self.cmd_view)
976
977 parser_log = subparsers.add_parser(
978 'log', help='Prints what has been done so far')
Kuang-che Wu8b654092018-11-09 17:56:25 +0800979 parser_log.add_argument('--before', type=float)
980 parser_log.add_argument('--after', type=float)
981 parser_log.add_argument(
982 '--json', action='store_true', help='Machine readable output')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800983 parser_log.set_defaults(func=self.cmd_log)
984
985 parser_next = subparsers.add_parser(
986 'next', help='Prints next suggested rev to bisect')
987 parser_next.set_defaults(func=self.cmd_next)
988
989 return parser
990
991 def main(self, *args, **kwargs):
992 """Command line main function.
993
994 Args:
995 *args: Command line arguments.
996 **kwargs: additional non command line arguments passed by script code.
997 {
998 'prog': Program name; optional.
999 }
1000 """
Kuang-che Wu385279d2017-09-27 14:48:28 +08001001 common.init()
Kuang-che Wu88875db2017-07-20 10:47:53 +08001002 parser = self.create_argument_parser(kwargs.get('prog'))
1003 opts = parser.parse_args(args or None)
1004 common.config_logging(opts)
1005
1006 self._create_states(session=opts.session, session_base=opts.session_base)
1007 if opts.command not in ('init', 'reset', 'config'):
1008 self.states.load()
1009 self.domain = self.domain_cls(self.states.config)
Kuang-che Wu4f6f9122019-04-23 17:44:46 +08001010 self.strategy = self._strategy_factory()
Kuang-che Wu88875db2017-07-20 10:47:53 +08001011
1012 return opts.func(opts)