1#!/usr/bin/env python3 2# Copyright 2020 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Emails the mage if PGO profile generation hasn't succeeded recently.""" 7 8# pylint: disable=cros-logging-import 9 10import argparse 11import datetime 12import sys 13import subprocess 14import logging 15from typing import List, NamedTuple, Optional, Tuple 16 17from cros_utils import email_sender 18from cros_utils import tiny_render 19 20PGO_BUILDBOT_LINK = ('https://ci.chromium.org/p/chromeos/builders/toolchain/' 21 'pgo-generate-llvm-next-orchestrator') 22 23 24class ProfdataInfo(NamedTuple): 25 """Data about an llvm profdata in our gs:// bucket.""" 26 date: datetime.datetime 27 location: str 28 29 30def parse_date(date: str) -> datetime.datetime: 31 time_format = '%Y-%m-%dT%H:%M:%SZ' 32 if not date.endswith('Z'): 33 time_format += '%z' 34 return datetime.datetime.strptime(date, time_format) 35 36 37def fetch_most_recent_profdata(arch: str) -> ProfdataInfo: 38 result = subprocess.run( 39 [ 40 'gsutil.py', 41 'ls', 42 '-l', 43 f'gs://chromeos-toolchain-artifacts/llvm-pgo/{arch}/' 44 '*.profdata.tar.xz', 45 ], 46 check=True, 47 stdout=subprocess.PIPE, 48 encoding='utf-8', 49 ) 50 51 # Each line will be a profdata; the last one is a summary, so drop it. 52 infos = [] 53 for rec in result.stdout.strip().splitlines()[:-1]: 54 _size, date, url = rec.strip().split() 55 infos.append(ProfdataInfo(date=parse_date(date), location=url)) 56 return max(infos) 57 58 59def compose_complaint_email( 60 out_of_date_profiles: List[Tuple[datetime.datetime, ProfdataInfo]] 61) -> Optional[Tuple[str, tiny_render.Piece]]: 62 if not out_of_date_profiles: 63 return None 64 65 if len(out_of_date_profiles) == 1: 66 subject = '1 llvm profile is out of date' 67 body = ['out-of-date profile:'] 68 else: 69 subject = f'{len(out_of_date_profiles)} llvm profiles are out of date' 70 body = ['out-of-date profiles:'] 71 72 out_of_date_items = [] 73 for arch, profdata_info in out_of_date_profiles: 74 out_of_date_items.append( 75 f'{arch} (most recent profile was from {profdata_info.date} at ' 76 f'{profdata_info.location!r})') 77 78 body += [ 79 tiny_render.UnorderedList(out_of_date_items), 80 tiny_render.line_break, 81 tiny_render.line_break, 82 'PTAL to see if the llvm-pgo-generate bots are functioning normally. ' 83 'Their status can be found at ', 84 tiny_render.Link(href=PGO_BUILDBOT_LINK, inner=PGO_BUILDBOT_LINK), 85 '.', 86 ] 87 return subject, body 88 89 90def main() -> None: 91 logging.basicConfig(level=logging.INFO) 92 93 parser = argparse.ArgumentParser( 94 description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) 95 parser.add_argument( 96 '--dry_run', 97 action='store_true', 98 help="Don't actually send an email", 99 ) 100 parser.add_argument( 101 '--max_age_days', 102 # These builders run ~weekly. If we fail to generate two in a row, 103 # something's probably wrong. 104 default=15, 105 type=int, 106 help='How old to let profiles get before complaining, in days', 107 ) 108 args = parser.parse_args() 109 110 now = datetime.datetime.now() 111 logging.info('Start time is %r', now) 112 113 max_age = datetime.timedelta(days=args.max_age_days) 114 out_of_date_profiles = [] 115 for arch in ('arm', 'arm64', 'amd64'): 116 logging.info('Fetching most recent profdata for %r', arch) 117 most_recent = fetch_most_recent_profdata(arch) 118 logging.info('Most recent profdata for %r is %r', arch, most_recent) 119 120 age = now - most_recent.date 121 if age >= max_age: 122 out_of_date_profiles.append((arch, most_recent)) 123 124 email = compose_complaint_email(out_of_date_profiles) 125 if not email: 126 logging.info('No email to send; quit') 127 return 128 129 subject, body = email 130 131 identifier = 'llvm-pgo-monitor' 132 subject = f'[{identifier}] {subject}' 133 134 logging.info('Sending email with title %r', subject) 135 if args.dry_run: 136 logging.info('Dry run specified\nSubject: %s\nBody:\n%s', subject, 137 tiny_render.render_text_pieces(body)) 138 else: 139 email_sender.EmailSender().SendX20Email( 140 subject=subject, 141 identifier=identifier, 142 well_known_recipients=['mage'], 143 direct_recipients=['gbiv@google.com'], 144 text_body=tiny_render.render_text_pieces(body), 145 html_body=tiny_render.render_html_pieces(body), 146 ) 147 148 149if __name__ == '__main__': 150 sys.exit(main()) 151