blob: 9cfcabf5ca98c412b144a180e6e730c3e2c532ce [file] [log] [blame]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2021 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import csv
import json
import pathlib
import subprocess
import sys
from collections import defaultdict
def load_buildstats(gsutil, bid):
gs_paths = [
f"gs://fuchsia-artifacts/builds/{bid}/fuchsia-buildstats.json",
f"gs://fuchsia-artifacts-internal/builds/{bid}/fuchsia-buildstats.json",
]
for p in gs_paths:
cmd = subprocess.run([gsutil, "cat", p], capture_output=True)
if cmd.returncode == 0:
return json.loads(cmd.stdout)
raise Exception(f"buildstats.json for {bid} not found, tried: {gs_paths}")
def main():
parser = argparse.ArgumentParser(
description="Count and aggregate stats of critical actions through multiple builds specified by build IDs from a file. This script can take a long time to finish because it takes a few seconds to load and process one build.",
)
parser.add_argument(
"--build_ids",
help="Path to build IDs file, one build ID per line",
type=pathlib.Path,
required=True,
)
parser.add_argument(
"--gsutil",
help="Path to gsutil, see https://cloud.google.com/storage/docs/gsutil_install#install",
type=pathlib.Path,
required=True,
)
parser.add_argument(
"--output",
help="Path to output results to, output will be in CSV format",
type=pathlib.Path,
required=True,
)
parser.add_argument(
"-v",
"--verbose",
dest="verbose",
action="store_true",
help="Print extra information when this script is running",
)
parser.add_argument(
"--skip_if_not_found",
dest="skip_if_not_found",
action="store_true",
help="Skip a build if build stats are not found",
)
parser.set_defaults(verbose=False)
parser.set_defaults(skip_if_not_found=False)
args = parser.parse_args()
with open(args.build_ids, "r") as f:
bids = [bid.strip() for bid in f.readlines()]
if not bids:
raise Exception(f"No build IDs found in {args.build_ids}")
counts = defaultdict(int)
drags = defaultdict(list)
durations = defaultdict(list)
i, tot = 0, len(bids)
if args.verbose:
print(f"{tot} builds to load and process ...")
for bid in bids:
i += 1
if args.verbose:
print(
f"[{i}/{tot}] Loading and counting critical actions of build {bid} ..."
)
try:
buildstats = load_buildstats(args.gsutil, bid)
except Exception as err:
if args.skip_if_not_found:
print(f"Skipping build {bid}: {err}")
continue
print(
"Tip: set --skip_if_not_found to continue on failures like this"
)
raise err
for step in buildstats["CriticalPath"]:
outputs = ",".join(sorted(step["Outputs"]))
counts[outputs] += 1
drags[outputs].append(step["Drag"])
durations[outputs].append(step["End"] - step["Start"])
if args.verbose:
print(f"Writing output CSV to {args.output} ...")
with open(args.output, "w") as f:
writer = csv.writer(f)
writer.writerow(
["outputs", "count", "average_duration_ms", "average_drag_ms"]
)
sorted_counts = sorted(counts.items(), key=lambda c: c[1], reverse=True)
for outputs, count in sorted_counts:
avg_drag = sum(drags[outputs]) / len(drags[outputs]) / 1000000
avg_dur = (
sum(durations[outputs]) / len(durations[outputs]) / 1000000
)
writer.writerow([outputs, count, avg_dur, avg_drag])
if args.verbose:
print("DONE")
if __name__ == "__main__":
sys.exit(main())