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