| #!/usr/bin/env python3 |
| # Copyright 2020 The Chromium OS Authors. All rights reserved. |
| # 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.""" |
| |
| # pylint: disable=cros-logging-import |
| |
| import argparse |
| import datetime |
| import sys |
| import subprocess |
| import logging |
| from typing import List, NamedTuple, Optional, Tuple |
| |
| from cros_utils import email_sender |
| from cros_utils import tiny_render |
| |
| 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_email( |
| out_of_date_profiles: List[Tuple[datetime.datetime, ProfdataInfo]] |
| ) -> Optional[Tuple[str, tiny_render.Piece]]: |
| if not out_of_date_profiles: |
| return None |
| |
| if len(out_of_date_profiles) == 1: |
| subject = '1 llvm profile is out of date' |
| body = ['out-of-date profile:'] |
| else: |
| subject = f'{len(out_of_date_profiles)} llvm profiles are out of date' |
| body = ['out-of-date profiles:'] |
| |
| out_of_date_items = [] |
| for arch, profdata_info in out_of_date_profiles: |
| out_of_date_items.append( |
| f'{arch} (most recent profile was from {profdata_info.date} at ' |
| f'{profdata_info.location!r})') |
| |
| body += [ |
| tiny_render.UnorderedList(out_of_date_items), |
| tiny_render.line_break, |
| tiny_render.line_break, |
| 'PTAL to see if the llvm-pgo-generate bots are functioning normally. ' |
| 'Their status can be found at ', |
| tiny_render.Link(href=PGO_BUILDBOT_LINK, inner=PGO_BUILDBOT_LINK), |
| '.', |
| ] |
| return subject, body |
| |
| |
| def main() -> None: |
| logging.basicConfig(level=logging.INFO) |
| |
| parser = argparse.ArgumentParser( |
| description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) |
| parser.add_argument( |
| '--dry_run', |
| action='store_true', |
| help="Don't actually send an email", |
| ) |
| 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)) |
| |
| email = compose_complaint_email(out_of_date_profiles) |
| if not email: |
| logging.info('No email to send; quit') |
| return |
| |
| subject, body = email |
| |
| identifier = 'llvm-pgo-monitor' |
| subject = f'[{identifier}] {subject}' |
| |
| logging.info('Sending email with title %r', subject) |
| if args.dry_run: |
| logging.info('Dry run specified\nSubject: %s\nBody:\n%s', subject, |
| tiny_render.render_text_pieces(body)) |
| else: |
| email_sender.EmailSender().SendX20Email( |
| subject=subject, |
| identifier=identifier, |
| well_known_recipients=['mage'], |
| direct_recipients=['gbiv@google.com'], |
| text_body=tiny_render.render_text_pieces(body), |
| html_body=tiny_render.render_html_pieces(body), |
| ) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |