xref: /openbmc/openbmc/poky/scripts/buildstats-summary (revision 8460358c3d24c71d9d38fd126c745854a6301564)
1#!/usr/bin/env python3
2#
3# Dump a summary of the specified buildstats to the terminal, filtering and
4# sorting by walltime.
5#
6# SPDX-License-Identifier: GPL-2.0-only
7
8import argparse
9import dataclasses
10import datetime
11import enum
12import os
13import pathlib
14import sys
15
16scripts_path = os.path.dirname(os.path.realpath(__file__))
17sys.path.append(os.path.join(scripts_path, "lib"))
18import buildstats
19
20
21@dataclasses.dataclass
22class Task:
23    recipe: str
24    task: str
25    start: datetime.datetime
26    duration: datetime.timedelta
27
28
29class Sorting(enum.Enum):
30    start = 1
31    duration = 2
32
33    # argparse integration
34    def __str__(self) -> str:
35        return self.name
36
37    def __repr__(self) -> str:
38        return self.name
39
40    @staticmethod
41    def from_string(s: str):
42        try:
43            return Sorting[s]
44        except KeyError:
45            return s
46
47
48def read_buildstats(path: pathlib.Path) -> buildstats.BuildStats:
49    if not path.exists():
50        raise Exception(f"No such file or directory: {path}")
51    if path.is_file():
52        return buildstats.BuildStats.from_file_json(path)
53    if (path / "build_stats").is_file():
54        return buildstats.BuildStats.from_dir(path)
55    raise Exception(f"Cannot find buildstats in {path}")
56
57
58def dump_buildstats(args, bs: buildstats.BuildStats):
59    tasks = []
60    for recipe in bs.values():
61        for task, stats in recipe.tasks.items():
62            t = Task(
63                recipe.name,
64                task,
65                datetime.datetime.fromtimestamp(stats["start_time"]),
66                datetime.timedelta(seconds=int(stats.walltime)),
67            )
68            tasks.append(t)
69
70    tasks.sort(key=lambda t: getattr(t, args.sort.name))
71
72    minimum = datetime.timedelta(seconds=args.shortest)
73    highlight = datetime.timedelta(seconds=args.highlight)
74
75    for t in tasks:
76        if t.duration >= minimum:
77            line = f"{t.duration}    {t.recipe}:{t.task}"
78            if args.highlight and t.duration >= highlight:
79                print(f"\033[1m{line}\033[0m")
80            else:
81                print(line)
82
83
84def main(argv=None) -> int:
85    parser = argparse.ArgumentParser(
86        formatter_class=argparse.ArgumentDefaultsHelpFormatter
87    )
88
89    parser.add_argument(
90        "buildstats",
91        metavar="BUILDSTATS",
92        nargs="?",
93        type=pathlib.Path,
94        help="Buildstats file, or latest if not specified",
95    )
96    parser.add_argument(
97        "--sort",
98        "-s",
99        type=Sorting.from_string,
100        choices=list(Sorting),
101        default=Sorting.start,
102        help="Sort tasks",
103    )
104    parser.add_argument(
105        "--shortest",
106        "-t",
107        type=int,
108        default=1,
109        metavar="SECS",
110        help="Hide tasks shorter than SECS seconds",
111    )
112    parser.add_argument(
113        "--highlight",
114        "-g",
115        type=int,
116        default=60,
117        metavar="SECS",
118        help="Highlight tasks longer than SECS seconds (0 disabled)",
119    )
120
121    args = parser.parse_args(argv)
122
123    # If a buildstats file wasn't specified, try to find the last one
124    if not args.buildstats:
125        try:
126            builddir = pathlib.Path(os.environ["BUILDDIR"])
127            buildstats_dir = builddir / "tmp" / "buildstats"
128            args.buildstats = sorted(buildstats_dir.iterdir())[-1]
129        except KeyError:
130            print("Build environment has not been configured, cannot find buildstats")
131            return 1
132
133    bs = read_buildstats(args.buildstats)
134    dump_buildstats(args, bs)
135
136    return 0
137
138
139if __name__ == "__main__":
140    sys.exit(main())
141