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