xref: /aosp_15_r20/external/perfetto/tools/bisect_ui_releases (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1#!/usr/bin/env python3
2# Copyright (C) 2024 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""
16Runs a bisection on the autopush ui.perfetto.dev builds
17
18Similar to git bisect, but bisects UI releases rather than commits.
19This works only for autopush builds from main, ignores canary and stable
20channels, as they make the history non-linear.
21
22How it works:
23- It first obtains an unordered list of versions from gs://ui.perfetto.dev
24- Then obtains the list of ordered commits from git
25- Intersects the two lists, keeping only git commits that have a corresponding
26  ui autopush release.
27- Proceeds with a guided bisect in the range.
28"""
29
30import argparse
31import sys
32
33from subprocess import check_output
34
35COMMIT_ABBR_LEN = 9  # UI truncates commitish to 9 chars, e.g. v45.0-38b7c2b12.
36
37
38def main():
39  parser = argparse.ArgumentParser()
40  parser.add_argument(
41      '--good',
42      default=None,
43      help='Last good release (e.g. v44.0-257a02990).' +
44      'Defaults to the first verion available')
45  parser.add_argument(
46      '--bad',
47      default=None,
48      help='First bad release. Defaults to the latest version available')
49  args = parser.parse_args()
50
51  print('Fetching list of UI releases from GCS...')
52  rev_list = check_output(['gsutil.py', 'ls', 'gs://ui.perfetto.dev/']).decode()
53  ui_map = {}  # maps '38b7c2b12' -> 'v45.0-38b7c2b12'
54  for line in rev_list.split():
55    version = line.split('/')[3]
56    if '-' not in version:
57      continue
58    ver_hash = version.split('-')[1]
59    ui_map[ver_hash] = version
60  print('Found %d UI versions' % len(ui_map))
61
62  # Get the linear history of all commits.
63  print('Fetching revision history from git...')
64  ui_versions = []
65  git_out = check_output(['git', 'rev-list', '--left-only',
66                          'origin/main']).decode()
67  for line in git_out.split():
68    line = line.strip()
69    rev = line[0:COMMIT_ABBR_LEN]
70    if rev not in ui_map:
71      continue  # Not all perfetto commits have a UI autopush build.
72    ui_versions.append(ui_map[rev])
73
74  # git rev-list emits entries in recent -> older versions. Reverse it.
75  ui_versions.reverse()
76
77  # Note that not all the entries in ui_map will be present in ui_versions.
78  # This is because ui_map contains also builds coming from canary and stable
79  # branches, that we ignore here.
80
81  start = ui_versions.index(args.good) if args.good else 0
82  end = ui_versions.index(args.bad) if args.bad else len(ui_versions) - 1
83  while end - start > 1:
84    print('\033c', end='')  # clear terminal.
85    print(
86        'Bisecting from %s (last good) to %s (first bad), %d revisions to go' %
87        (ui_versions[start], ui_versions[end], end - start + 1))
88    mid = (end + start) // 2
89
90    # Print a visual indication of where we are in the bisect.
91    for i in reversed(range(start, end + 1)):
92      sfx = ''
93      if i == start:
94        sfx = ' GOOD --------------'
95      elif i == end:
96        sfx = ' BAD ---------------'
97      elif i == mid:
98        sfx = ' <- version to test'
99      print(ui_versions[i] + sfx)
100
101    user_feedback = input(
102        'https://ui.perfetto.dev/%s/. Type g for good and b for bad: ' %
103        ui_versions[mid])
104    if user_feedback == 'b':
105      end = mid
106    elif user_feedback == 'g':
107      start = mid
108    else:
109      print('Unrecognised key "%d", try again' % user_feedback)
110
111  print('First bad UI release %s' % ui_versions[end])
112  print('You should now inspect the individual commits via git log good..bad')
113
114
115if __name__ == '__main__':
116  sys.exit(main())
117