xref: /aosp_15_r20/external/mesa3d/bin/ci/ci_gantt_chart.py (revision 6104692788411f58d303aa86923a9ff6ecaded22)
1*61046927SAndroid Build Coastguard Worker#!/usr/bin/env python3
2*61046927SAndroid Build Coastguard Worker# Copyright © 2023 Collabora Ltd.
3*61046927SAndroid Build Coastguard Worker# Authors:
4*61046927SAndroid Build Coastguard Worker#   Helen Koike <[email protected]>
5*61046927SAndroid Build Coastguard Worker#
6*61046927SAndroid Build Coastguard Worker# For the dependencies, see the requirements.txt
7*61046927SAndroid Build Coastguard Worker# SPDX-License-Identifier: MIT
8*61046927SAndroid Build Coastguard Worker
9*61046927SAndroid Build Coastguard Worker
10*61046927SAndroid Build Coastguard Workerimport argparse
11*61046927SAndroid Build Coastguard Workerimport gitlab
12*61046927SAndroid Build Coastguard Workerimport plotly.express as px
13*61046927SAndroid Build Coastguard Workerfrom gitlab_common import pretty_duration
14*61046927SAndroid Build Coastguard Workerfrom datetime import datetime, timedelta
15*61046927SAndroid Build Coastguard Workerfrom gitlab_common import read_token, GITLAB_URL, get_gitlab_pipeline_from_url
16*61046927SAndroid Build Coastguard Worker
17*61046927SAndroid Build Coastguard Worker
18*61046927SAndroid Build Coastguard Workerdef calculate_queued_at(job):
19*61046927SAndroid Build Coastguard Worker    # we can have queued_duration without started_at when a job is canceled
20*61046927SAndroid Build Coastguard Worker    if not job.queued_duration or not job.started_at:
21*61046927SAndroid Build Coastguard Worker        return None
22*61046927SAndroid Build Coastguard Worker    started_at = job.started_at.replace("Z", "+00:00")
23*61046927SAndroid Build Coastguard Worker    return datetime.fromisoformat(started_at) - timedelta(seconds=job.queued_duration)
24*61046927SAndroid Build Coastguard Worker
25*61046927SAndroid Build Coastguard Worker
26*61046927SAndroid Build Coastguard Workerdef calculate_time_difference(time1, time2):
27*61046927SAndroid Build Coastguard Worker    if not time1 or not time2:
28*61046927SAndroid Build Coastguard Worker        return None
29*61046927SAndroid Build Coastguard Worker    if type(time1) is str:
30*61046927SAndroid Build Coastguard Worker        time1 = datetime.fromisoformat(time1.replace("Z", "+00:00"))
31*61046927SAndroid Build Coastguard Worker    if type(time2) is str:
32*61046927SAndroid Build Coastguard Worker        time2 = datetime.fromisoformat(time2.replace("Z", "+00:00"))
33*61046927SAndroid Build Coastguard Worker
34*61046927SAndroid Build Coastguard Worker    diff = time2 - time1
35*61046927SAndroid Build Coastguard Worker    return pretty_duration(diff.seconds)
36*61046927SAndroid Build Coastguard Worker
37*61046927SAndroid Build Coastguard Worker
38*61046927SAndroid Build Coastguard Workerdef create_task_name(job):
39*61046927SAndroid Build Coastguard Worker    status_color = {"success": "green", "failed": "red"}.get(job.status, "grey")
40*61046927SAndroid Build Coastguard Worker    return f"{job.name}\t(<span style='color: {status_color}'>{job.status}</span>,<a href='{job.web_url}'>{job.id}</a>)"
41*61046927SAndroid Build Coastguard Worker
42*61046927SAndroid Build Coastguard Worker
43*61046927SAndroid Build Coastguard Workerdef add_gantt_bar(job, tasks):
44*61046927SAndroid Build Coastguard Worker    queued_at = calculate_queued_at(job)
45*61046927SAndroid Build Coastguard Worker    task_name = create_task_name(job)
46*61046927SAndroid Build Coastguard Worker
47*61046927SAndroid Build Coastguard Worker    tasks.append(
48*61046927SAndroid Build Coastguard Worker        {
49*61046927SAndroid Build Coastguard Worker            "Job": task_name,
50*61046927SAndroid Build Coastguard Worker            "Start": job.created_at,
51*61046927SAndroid Build Coastguard Worker            "Finish": queued_at,
52*61046927SAndroid Build Coastguard Worker            "Duration": calculate_time_difference(job.created_at, queued_at),
53*61046927SAndroid Build Coastguard Worker            "Phase": "Waiting dependencies",
54*61046927SAndroid Build Coastguard Worker        }
55*61046927SAndroid Build Coastguard Worker    )
56*61046927SAndroid Build Coastguard Worker    tasks.append(
57*61046927SAndroid Build Coastguard Worker        {
58*61046927SAndroid Build Coastguard Worker            "Job": task_name,
59*61046927SAndroid Build Coastguard Worker            "Start": queued_at,
60*61046927SAndroid Build Coastguard Worker            "Finish": job.started_at,
61*61046927SAndroid Build Coastguard Worker            "Duration": calculate_time_difference(queued_at, job.started_at),
62*61046927SAndroid Build Coastguard Worker            "Phase": "Queued",
63*61046927SAndroid Build Coastguard Worker        }
64*61046927SAndroid Build Coastguard Worker    )
65*61046927SAndroid Build Coastguard Worker    tasks.append(
66*61046927SAndroid Build Coastguard Worker        {
67*61046927SAndroid Build Coastguard Worker            "Job": task_name,
68*61046927SAndroid Build Coastguard Worker            "Start": job.started_at,
69*61046927SAndroid Build Coastguard Worker            "Finish": job.finished_at,
70*61046927SAndroid Build Coastguard Worker            "Duration": calculate_time_difference(job.started_at, job.finished_at),
71*61046927SAndroid Build Coastguard Worker            "Phase": "Running",
72*61046927SAndroid Build Coastguard Worker        }
73*61046927SAndroid Build Coastguard Worker    )
74*61046927SAndroid Build Coastguard Worker
75*61046927SAndroid Build Coastguard Worker
76*61046927SAndroid Build Coastguard Workerdef generate_gantt_chart(pipeline):
77*61046927SAndroid Build Coastguard Worker    if pipeline.yaml_errors:
78*61046927SAndroid Build Coastguard Worker        raise ValueError("Pipeline YAML errors detected")
79*61046927SAndroid Build Coastguard Worker
80*61046927SAndroid Build Coastguard Worker    # Convert the data into a list of dictionaries for plotly
81*61046927SAndroid Build Coastguard Worker    tasks = []
82*61046927SAndroid Build Coastguard Worker
83*61046927SAndroid Build Coastguard Worker    for job in pipeline.jobs.list(all=True, include_retried=True):
84*61046927SAndroid Build Coastguard Worker        add_gantt_bar(job, tasks)
85*61046927SAndroid Build Coastguard Worker
86*61046927SAndroid Build Coastguard Worker    # Make it easier to see retried jobs
87*61046927SAndroid Build Coastguard Worker    tasks.sort(key=lambda x: x["Job"])
88*61046927SAndroid Build Coastguard Worker
89*61046927SAndroid Build Coastguard Worker    title = f"Gantt chart of jobs in pipeline <a href='{pipeline.web_url}'>{pipeline.web_url}</a>."
90*61046927SAndroid Build Coastguard Worker    title += (
91*61046927SAndroid Build Coastguard Worker        f" Total duration {str(timedelta(seconds=pipeline.duration))}"
92*61046927SAndroid Build Coastguard Worker        if pipeline.duration
93*61046927SAndroid Build Coastguard Worker        else ""
94*61046927SAndroid Build Coastguard Worker    )
95*61046927SAndroid Build Coastguard Worker
96*61046927SAndroid Build Coastguard Worker    # Create a Gantt chart
97*61046927SAndroid Build Coastguard Worker    fig = px.timeline(
98*61046927SAndroid Build Coastguard Worker        tasks,
99*61046927SAndroid Build Coastguard Worker        x_start="Start",
100*61046927SAndroid Build Coastguard Worker        x_end="Finish",
101*61046927SAndroid Build Coastguard Worker        y="Job",
102*61046927SAndroid Build Coastguard Worker        color="Phase",
103*61046927SAndroid Build Coastguard Worker        title=title,
104*61046927SAndroid Build Coastguard Worker        hover_data=["Duration"],
105*61046927SAndroid Build Coastguard Worker    )
106*61046927SAndroid Build Coastguard Worker
107*61046927SAndroid Build Coastguard Worker    # Calculate the height dynamically
108*61046927SAndroid Build Coastguard Worker    fig.update_layout(height=len(tasks) * 10, yaxis_tickfont_size=14)
109*61046927SAndroid Build Coastguard Worker
110*61046927SAndroid Build Coastguard Worker    # Add a deadline line to the chart
111*61046927SAndroid Build Coastguard Worker    created_at = datetime.fromisoformat(pipeline.created_at.replace("Z", "+00:00"))
112*61046927SAndroid Build Coastguard Worker    timeout_at = created_at + timedelta(hours=1)
113*61046927SAndroid Build Coastguard Worker    fig.add_vrect(
114*61046927SAndroid Build Coastguard Worker        x0=timeout_at,
115*61046927SAndroid Build Coastguard Worker        x1=timeout_at,
116*61046927SAndroid Build Coastguard Worker        annotation_text="1h Timeout",
117*61046927SAndroid Build Coastguard Worker        fillcolor="gray",
118*61046927SAndroid Build Coastguard Worker        line_width=2,
119*61046927SAndroid Build Coastguard Worker        line_color="gray",
120*61046927SAndroid Build Coastguard Worker        line_dash="dash",
121*61046927SAndroid Build Coastguard Worker        annotation_position="top left",
122*61046927SAndroid Build Coastguard Worker        annotation_textangle=90,
123*61046927SAndroid Build Coastguard Worker    )
124*61046927SAndroid Build Coastguard Worker
125*61046927SAndroid Build Coastguard Worker    return fig
126*61046927SAndroid Build Coastguard Worker
127*61046927SAndroid Build Coastguard Worker
128*61046927SAndroid Build Coastguard Workerdef parse_args() -> None:
129*61046927SAndroid Build Coastguard Worker    parser = argparse.ArgumentParser(
130*61046927SAndroid Build Coastguard Worker        description="Generate the Gantt chart from a given pipeline."
131*61046927SAndroid Build Coastguard Worker    )
132*61046927SAndroid Build Coastguard Worker    parser.add_argument("pipeline_url", type=str, help="URLs to the pipeline.")
133*61046927SAndroid Build Coastguard Worker    parser.add_argument(
134*61046927SAndroid Build Coastguard Worker        "-o",
135*61046927SAndroid Build Coastguard Worker        "--output",
136*61046927SAndroid Build Coastguard Worker        type=str,
137*61046927SAndroid Build Coastguard Worker        help="Output file name. Use html ou image suffixes to choose the format.",
138*61046927SAndroid Build Coastguard Worker    )
139*61046927SAndroid Build Coastguard Worker    parser.add_argument(
140*61046927SAndroid Build Coastguard Worker        "--token",
141*61046927SAndroid Build Coastguard Worker        metavar="token",
142*61046927SAndroid Build Coastguard Worker        help="force GitLab token, otherwise it's read from ~/.config/gitlab-token",
143*61046927SAndroid Build Coastguard Worker    )
144*61046927SAndroid Build Coastguard Worker    return parser.parse_args()
145*61046927SAndroid Build Coastguard Worker
146*61046927SAndroid Build Coastguard Worker
147*61046927SAndroid Build Coastguard Workerif __name__ == "__main__":
148*61046927SAndroid Build Coastguard Worker    args = parse_args()
149*61046927SAndroid Build Coastguard Worker
150*61046927SAndroid Build Coastguard Worker    token = read_token(args.token)
151*61046927SAndroid Build Coastguard Worker
152*61046927SAndroid Build Coastguard Worker    gl = gitlab.Gitlab(url=GITLAB_URL, private_token=token, retry_transient_errors=True)
153*61046927SAndroid Build Coastguard Worker
154*61046927SAndroid Build Coastguard Worker    pipeline, _ = get_gitlab_pipeline_from_url(gl, args.pipeline_url)
155*61046927SAndroid Build Coastguard Worker    fig = generate_gantt_chart(pipeline)
156*61046927SAndroid Build Coastguard Worker    if args.output and "htm" in args.output:
157*61046927SAndroid Build Coastguard Worker        fig.write_html(args.output)
158*61046927SAndroid Build Coastguard Worker    elif args.output:
159*61046927SAndroid Build Coastguard Worker        fig.update_layout(width=1000)
160*61046927SAndroid Build Coastguard Worker        fig.write_image(args.output)
161*61046927SAndroid Build Coastguard Worker    else:
162*61046927SAndroid Build Coastguard Worker        fig.show()
163