| #!/usr/bin/env python3 |
| # Copyright (C) 2024 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """ |
| Runs a bisection on the autopush ui.perfetto.dev builds |
| |
| Similar to git bisect, but bisects UI releases rather than commits. |
| This works only for autopush builds from main, ignores canary and stable |
| channels, as they make the history non-linear. |
| |
| How it works: |
| - It first obtains an unordered list of versions from gs://ui.perfetto.dev |
| - Then obtains the list of ordered commits from git |
| - Intersects the two lists, keeping only git commits that have a corresponding |
| ui autopush release. |
| - Proceeds with a guided bisect in the range. |
| """ |
| |
| import argparse |
| import sys |
| |
| from subprocess import check_output |
| |
| COMMIT_ABBR_LEN = 9 # UI truncates commitish to 9 chars, e.g. v45.0-38b7c2b12. |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| '--good', |
| default=None, |
| help='Last good release (e.g. v44.0-257a02990).' + |
| 'Defaults to the first verion available') |
| parser.add_argument( |
| '--bad', |
| default=None, |
| help='First bad release. Defaults to the latest version available') |
| args = parser.parse_args() |
| |
| print('Fetching list of UI releases from GCS...') |
| rev_list = check_output(['gsutil.py', 'ls', 'gs://ui.perfetto.dev/']).decode() |
| ui_map = {} # maps '38b7c2b12' -> 'v45.0-38b7c2b12' |
| for line in rev_list.split(): |
| version = line.split('/')[3] |
| if '-' not in version: |
| continue |
| ver_hash = version.split('-')[1] |
| ui_map[ver_hash] = version |
| print('Found %d UI versions' % len(ui_map)) |
| |
| # Get the linear history of all commits. |
| print('Fetching revision history from git...') |
| ui_versions = [] |
| git_out = check_output(['git', 'rev-list', '--left-only', |
| 'origin/main']).decode() |
| for line in git_out.split(): |
| line = line.strip() |
| rev = line[0:COMMIT_ABBR_LEN] |
| if rev not in ui_map: |
| continue # Not all perfetto commits have a UI autopush build. |
| ui_versions.append(ui_map[rev]) |
| |
| # git rev-list emits entries in recent -> older versions. Reverse it. |
| ui_versions.reverse() |
| |
| # Note that not all the entries in ui_map will be present in ui_versions. |
| # This is because ui_map contains also builds coming from canary and stable |
| # branches, that we ignore here. |
| |
| start = ui_versions.index(args.good) if args.good else 0 |
| end = ui_versions.index(args.bad) if args.bad else len(ui_versions) - 1 |
| while end - start > 1: |
| print('\033c', end='') # clear terminal. |
| print( |
| 'Bisecting from %s (last good) to %s (first bad), %d revisions to go' % |
| (ui_versions[start], ui_versions[end], end - start + 1)) |
| mid = (end + start) // 2 |
| |
| # Print a visual indication of where we are in the bisect. |
| for i in reversed(range(start, end + 1)): |
| sfx = '' |
| if i == start: |
| sfx = ' GOOD --------------' |
| elif i == end: |
| sfx = ' BAD ---------------' |
| elif i == mid: |
| sfx = ' <- version to test' |
| print(ui_versions[i] + sfx) |
| |
| user_feedback = input( |
| 'https://ui.perfetto.dev/%s/. Type g for good and b for bad: ' % |
| ui_versions[mid]) |
| if user_feedback == 'b': |
| end = mid |
| elif user_feedback == 'g': |
| start = mid |
| else: |
| print('Unrecognised key "%d", try again' % user_feedback) |
| |
| print('First bad UI release %s' % ui_versions[end]) |
| print('You should now inspect the individual commits via git log good..bad') |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |