| #!/usr/bin/env python3 |
| # Copyright 2020 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Emails the mage if PGO profile generation hasn't succeeded recently.""" |
| |
| import argparse |
| import datetime |
| import logging |
| import subprocess |
| import sys |
| from typing import List, NamedTuple, Optional, Tuple |
| |
| |
| PGO_BUILDBOT_LINK = ( |
| "https://ci.chromium.org/p/chromeos/builders/toolchain/" |
| "pgo-generate-llvm-next-orchestrator" |
| ) |
| |
| |
| class ProfdataInfo(NamedTuple): |
| """Data about an llvm profdata in our gs:// bucket.""" |
| |
| date: datetime.datetime |
| location: str |
| |
| |
| def parse_date(date: str) -> datetime.datetime: |
| time_format = "%Y-%m-%dT%H:%M:%SZ" |
| if not date.endswith("Z"): |
| time_format += "%z" |
| return datetime.datetime.strptime(date, time_format) |
| |
| |
| def fetch_most_recent_profdata(arch: str) -> ProfdataInfo: |
| result = subprocess.run( |
| [ |
| "gsutil.py", |
| "ls", |
| "-l", |
| f"gs://chromeos-toolchain-artifacts/llvm-pgo/{arch}/" |
| "*.profdata.tar.xz", |
| ], |
| check=True, |
| stdout=subprocess.PIPE, |
| encoding="utf-8", |
| ) |
| |
| # Each line will be a profdata; the last one is a summary, so drop it. |
| infos = [] |
| for rec in result.stdout.strip().splitlines()[:-1]: |
| _size, date, url = rec.strip().split() |
| infos.append(ProfdataInfo(date=parse_date(date), location=url)) |
| return max(infos) |
| |
| |
| def compose_complaint( |
| out_of_date_profiles: List[Tuple[datetime.datetime, ProfdataInfo]] |
| ) -> Optional[str]: |
| if not out_of_date_profiles: |
| return None |
| |
| if len(out_of_date_profiles) == 1: |
| body_lines = ["1 profile is out of date:"] |
| else: |
| body_lines = [f"{len(out_of_date_profiles)} profiles are out of date:"] |
| |
| for arch, profdata_info in out_of_date_profiles: |
| body_lines.append( |
| f"- {arch} (most recent profile was from {profdata_info.date} at " |
| f"{profdata_info.location!r})" |
| ) |
| |
| body_lines.append("\n") |
| body_lines.append( |
| "PTAL to see if the llvm-pgo-generate bots are functioning normally. " |
| f"Their status can be found at {PGO_BUILDBOT_LINK}." |
| ) |
| return "\n".join(body_lines) |
| |
| |
| def main() -> None: |
| logging.basicConfig(level=logging.INFO) |
| |
| parser = argparse.ArgumentParser( |
| description=__doc__, |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| ) |
| parser.add_argument( |
| "--max_age_days", |
| # These builders run ~weekly. If we fail to generate two in a row, |
| # something's probably wrong. |
| default=15, |
| type=int, |
| help="How old to let profiles get before complaining, in days", |
| ) |
| args = parser.parse_args() |
| |
| now = datetime.datetime.now() |
| logging.info("Start time is %r", now) |
| |
| max_age = datetime.timedelta(days=args.max_age_days) |
| out_of_date_profiles = [] |
| for arch in ("arm", "arm64", "amd64"): |
| logging.info("Fetching most recent profdata for %r", arch) |
| most_recent = fetch_most_recent_profdata(arch) |
| logging.info("Most recent profdata for %r is %r", arch, most_recent) |
| |
| age = now - most_recent.date |
| if age >= max_age: |
| out_of_date_profiles.append((arch, most_recent)) |
| |
| complaint = compose_complaint(out_of_date_profiles) |
| if complaint: |
| logging.error("%s", complaint) |
| sys.exit(1) |
| |
| logging.info("Nothing seems wrong") |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |