xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/auto_llvm_bisection.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1#!/usr/bin/env python3
2# Copyright 2019 The ChromiumOS Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Performs bisection on LLVM based off a .JSON file."""
7
8import enum
9import json
10import os
11import subprocess
12import sys
13import time
14import traceback
15
16import chroot
17import llvm_bisection
18import update_tryjob_status
19
20
21# Used to re-try for 'llvm_bisection.py' to attempt to launch more tryjobs.
22BISECTION_RETRY_TIME_SECS = 10 * 60
23
24# Wait time to then poll each tryjob whose 'status' value is 'pending'.
25POLL_RETRY_TIME_SECS = 30 * 60
26
27# The number of attempts for 'llvm_bisection.py' to launch more tryjobs.
28#
29# It is reset (break out of the `for` loop/ exit the program) if successfully
30# launched more tryjobs or bisection is finished (no more revisions between
31# start and end of the bisection).
32BISECTION_ATTEMPTS = 3
33
34# The limit for updating all tryjobs whose 'status' is 'pending'.
35#
36# If the time that has passed for polling exceeds this value, then the program
37# will exit with the appropriate exit code.
38POLLING_LIMIT_SECS = 18 * 60 * 60
39
40
41class BuilderStatus(enum.Enum):
42    """Actual values given via 'cros buildresult'."""
43
44    PASS = "pass"
45    FAIL = "fail"
46    RUNNING = "running"
47
48
49# Writing a dict with `.value`s spelled out makes `black`'s style conflict with
50# `cros lint`'s diagnostics.
51builder_status_mapping = {
52    a.value: b.value
53    for a, b in (
54        (BuilderStatus.PASS, update_tryjob_status.TryjobStatus.GOOD),
55        (BuilderStatus.FAIL, update_tryjob_status.TryjobStatus.BAD),
56        (BuilderStatus.RUNNING, update_tryjob_status.TryjobStatus.PENDING),
57    )
58}
59
60
61def GetBuildResult(chromeos_path, buildbucket_id):
62    """Returns the conversion of the result of 'cros buildresult'."""
63
64    # Calls 'cros buildresult' to get the status of the tryjob.
65    try:
66        tryjob_json = subprocess.check_output(
67            [
68                "cros",
69                "buildresult",
70                "--buildbucket-id",
71                str(buildbucket_id),
72                "--report",
73                "json",
74            ],
75            cwd=chromeos_path,
76            stderr=subprocess.STDOUT,
77            encoding="utf-8",
78        )
79    except subprocess.CalledProcessError as err:
80        if "No build found. Perhaps not started" not in err.output:
81            raise
82        return None
83
84    tryjob_content = json.loads(tryjob_json)
85
86    build_result = str(tryjob_content["%d" % buildbucket_id]["status"])
87
88    # The string returned by 'cros buildresult' might not be in the mapping.
89    if build_result not in builder_status_mapping:
90        raise ValueError(
91            '"cros buildresult" return value is invalid: %s' % build_result
92        )
93
94    return builder_status_mapping[build_result]
95
96
97def main():
98    """Bisects LLVM using the result of `cros buildresult` of each tryjob.
99
100    Raises:
101        AssertionError: The script was run inside the chroot.
102    """
103
104    chroot.VerifyOutsideChroot()
105
106    args_output = llvm_bisection.GetCommandLineArgs()
107
108    chroot.VerifyChromeOSRoot(args_output.chromeos_path)
109
110    if os.path.isfile(args_output.last_tested):
111        print("Resuming bisection for %s" % args_output.last_tested)
112    else:
113        print("Starting a new bisection for %s" % args_output.last_tested)
114
115    while True:
116        # Update the status of existing tryjobs
117        if os.path.isfile(args_output.last_tested):
118            update_start_time = time.time()
119            with open(args_output.last_tested, encoding="utf-8") as json_file:
120                json_dict = json.load(json_file)
121            while True:
122                print(
123                    '\nAttempting to update all tryjobs whose "status" is '
124                    '"pending":'
125                )
126                print("-" * 40)
127
128                completed = True
129                for tryjob in json_dict["jobs"]:
130                    if (
131                        tryjob["status"]
132                        == update_tryjob_status.TryjobStatus.PENDING.value
133                    ):
134                        status = GetBuildResult(
135                            args_output.chromeos_path, tryjob["buildbucket_id"]
136                        )
137                        if status:
138                            tryjob["status"] = status
139                        else:
140                            completed = False
141
142                print("-" * 40)
143
144                # Proceed to the next step if all the existing tryjobs have
145                # completed.
146                if completed:
147                    break
148
149                delta_time = time.time() - update_start_time
150
151                if delta_time > POLLING_LIMIT_SECS:
152                    # Something is wrong with updating the tryjobs's 'status'
153                    # via `cros buildresult` (e.g. network issue, etc.).
154                    sys.exit("Failed to update pending tryjobs.")
155
156                print("-" * 40)
157                print("Sleeping for %d minutes." % (POLL_RETRY_TIME_SECS // 60))
158                time.sleep(POLL_RETRY_TIME_SECS)
159
160            # There should always be update from the tryjobs launched in the
161            # last iteration.
162            temp_filename = "%s.new" % args_output.last_tested
163            with open(temp_filename, "w", encoding="utf-8") as temp_file:
164                json.dump(
165                    json_dict, temp_file, indent=4, separators=(",", ": ")
166                )
167            os.rename(temp_filename, args_output.last_tested)
168
169        # Launch more tryjobs.
170        bisection_complete = (
171            llvm_bisection.BisectionExitStatus.BISECTION_COMPLETE.value
172        )
173        for cur_try in range(1, BISECTION_ATTEMPTS + 1):
174            try:
175                print("\nAttempting to launch more tryjobs if possible:")
176                print("-" * 40)
177
178                bisection_ret = llvm_bisection.main(args_output)
179
180                print("-" * 40)
181
182                # Stop if the bisection has completed.
183                if bisection_ret == bisection_complete:
184                    sys.exit(0)
185
186                # Successfully launched more tryjobs.
187                break
188            except Exception:
189                traceback.print_exc()
190
191                print("-" * 40)
192
193                # Exceeded the number of times to launch more tryjobs.
194                if cur_try == BISECTION_ATTEMPTS:
195                    sys.exit("Unable to continue bisection.")
196
197                num_retries_left = BISECTION_ATTEMPTS - cur_try
198
199                print(
200                    "Retries left to continue bisection %d." % num_retries_left
201                )
202
203                print(
204                    "Sleeping for %d minutes."
205                    % (BISECTION_RETRY_TIME_SECS // 60)
206                )
207                time.sleep(BISECTION_RETRY_TIME_SECS)
208
209
210if __name__ == "__main__":
211    main()
212