1#!/usr/bin/env python
2
3import argparse
4import pandas as pd
5import re
6
7COLUMNS=('Event', 'Type', 'Tag', 'Value', 'Unit')
8PSI_TO_MONITOR='cpu_some_avg10'
9CUJ_COMPLETED_EVENT = 'CUJ completed'
10PSI_EXCEEDED_THRESHOLD_RE = re.compile(r'^PSI exceeded threshold: (?P<psi_value>\d+)%'
11                                       + r'\s+(?P<psi_type>.*)$')
12PSI_DROPPED_BELOW_THRESHOLD_RE = re.compile(r'^PSI dropped below threshold: (?P<psi_value>\d+)%'
13                                            + r'\s+(?P<psi_type>.*)$')
14PSI_REACHED_BASELINE_RE = re.compile(r'^PSI reached baseline across latest'
15                                     r'\s+(?P<latest_psi_entries>\d+)\s+entries$')
16PSI_MONITOR_TAG = 'psi_monitor'
17PSI_EVENT_TYPE = 'psi_event'
18LOGCAT_EVENT_TYPE = 'logcat_event'
19LOGCAT_EVENT_UNIT = 'Duration millis'
20
21
22class PsiKpi:
23  def __init__(self, psi_to_monitor):
24    self._cuj_completed_millis = None
25    self._exceeded_threshold_millis = None
26    self._exceeded_threshold_value = None
27    self._exceeded_threshold_type = None
28    self._dropped_below_threshold_millis = None
29    self._dropped_below_threshold_value = None
30    self._dropped_below_threshold_type = None
31    self._reached_baseline_millis = None
32    self._reached_baseline_latest_psi_entries = None
33    self._max_psi_value = 0.0
34    self._max_psi_value_after_dropped_below_threshold = 0.0
35    self._psi_to_monitor = psi_to_monitor
36    self._total_duration_above_80p_threshold_millis = 0.0
37    self._total_duration_above_50p_threshold_millis = 0.0
38    self._total_duration_above_30p_threshold_millis = 0.0
39
40
41  def _do_match_exceeded_threshold_millis(self, monitor_start_relative_millis, event_description):
42    m = PSI_EXCEEDED_THRESHOLD_RE.match(event_description)
43    if not m:
44      return False
45    self._exceeded_threshold_millis = monitor_start_relative_millis
46    matched = m.groupdict()
47    self._exceeded_threshold_value = matched['psi_value']
48    self._exceeded_threshold_type = matched['psi_type']
49    return True
50
51
52  def _do_match_dropped_below_threshold_millis(self, monitor_start_relative_millis,
53                                               event_description):
54    m = PSI_DROPPED_BELOW_THRESHOLD_RE.match(event_description)
55    if not m:
56      return False
57    self._dropped_below_threshold_millis = monitor_start_relative_millis
58    matched = m.groupdict()
59    self._dropped_below_threshold_value = matched['psi_value']
60    self._dropped_below_threshold_type = matched['psi_type']
61    return True
62
63
64  def _check_valid(self):
65    if self._cuj_completed_millis is None:
66      raise ValueError('CUJ completed event not found')
67    if self._exceeded_threshold_millis is None:
68      raise ValueError('PSI exceeded threshold event not found')
69    if self._dropped_below_threshold_millis is None:
70      raise ValueError('PSI dropped below threshold event not found')
71    if self._reached_baseline_millis is None:
72      raise ValueError('PSI reached baseline event not found')
73    if self._dropped_below_threshold_type != self._exceeded_threshold_type:
74      raise ValueError('PSI threshold type mismatch: {} != {}'.format(
75          self._dropped_below_threshold_type, self._exceeded_threshold_type))
76    if self._dropped_below_threshold_value != self._exceeded_threshold_value:
77      raise ValueError('PSI threshold value mismatch: {} != {}'.format(
78          self._dropped_below_threshold_value, self._exceeded_threshold_value))
79    if self._exceeded_threshold_millis >= self._dropped_below_threshold_millis:
80      raise ValueError(
81        'PSI exceeded threshold millis ({}) >= dropped below threshold millis ({})'.format(
82          self._exceeded_threshold_millis, self._dropped_below_threshold_millis))
83    if self._cuj_completed_millis > self._exceeded_threshold_millis:
84      raise ValueError('CUJ completed millis ({}) > exceeded threshold millis ({})'
85                      .format(self._cuj_completed_millis, self._exceeded_threshold_millis))
86    if self._cuj_completed_millis > self._reached_baseline_millis:
87      raise ValueError('CUJ completed millis ({}) > reached baseline millis ({})'
88                        .format(self._cuj_completed_millis, self._reached_baseline_millis))
89    if self._reached_baseline_total_duration_millis <= 0:
90      raise ValueError('Time taken to reach PSI baseline <= 0: {}'
91                        .format(self._reached_baseline_total_duration_millis))
92
93  def process_psi_df(self, psi_df):
94    if psi_df.empty:
95      return
96    self._max_psi_value = psi_df[self._psi_to_monitor].max()
97    prev_psi_value = 0.0
98    prev_monitor_start_relative_millis = 0.0
99    for index, row in psi_df.iterrows():
100      monitor_start_relative_millis = row['monitor_start_relative_millis']
101      event_descriptions = row['event_descriptions']
102      psi_value = row[self._psi_to_monitor]
103      cur_polling_duration_millis = (monitor_start_relative_millis
104        - prev_monitor_start_relative_millis)
105
106      if (psi_value >= 80.0 or prev_psi_value >= 80.0):
107        self._total_duration_above_80p_threshold_millis += cur_polling_duration_millis
108
109      if (psi_value >= 50.0 or prev_psi_value >= 50.0):
110        self._total_duration_above_50p_threshold_millis += cur_polling_duration_millis
111
112      if (psi_value >= 30.0 or prev_psi_value >= 30.0):
113        self._total_duration_above_30p_threshold_millis += cur_polling_duration_millis
114
115      for event_description in event_descriptions:
116        if self._dropped_below_threshold_millis:
117          self._max_psi_value_after_dropped_below_threshold = max(
118            self._max_psi_value_after_dropped_below_threshold, psi_value)
119
120        if self._cuj_completed_millis is None and event_description == CUJ_COMPLETED_EVENT:
121          self._cuj_completed_millis = monitor_start_relative_millis
122          continue
123
124        if self._exceeded_threshold_millis is None and self._do_match_exceeded_threshold_millis(
125          monitor_start_relative_millis, event_description):
126          continue
127
128        if (self._dropped_below_threshold_millis is None and
129          self._do_match_dropped_below_threshold_millis(monitor_start_relative_millis,
130            event_description)):
131          continue
132
133        if self._reached_baseline_millis is None:
134          m = PSI_REACHED_BASELINE_RE.match(event_description)
135          if not m:
136            continue
137          self._reached_baseline_millis = monitor_start_relative_millis
138          matched = m.groupdict()
139          self._reached_baseline_latest_psi_entries = int(matched['latest_psi_entries'])
140          prev_entries = self._reached_baseline_latest_psi_entries
141          if prev_entries > index:
142            raise ValueError('Previous N PSI entries {} used in baseline calculation is greater '
143                             + 'than index {}'.format(prev_entries, index))
144          self._reached_baseline_total_duration_millis = (self._reached_baseline_millis -
145            psi_df.loc[index - prev_entries, 'monitor_start_relative_millis'])
146
147      prev_psi_value = psi_value
148      prev_monitor_start_relative_millis = monitor_start_relative_millis
149
150
151  def populate_kpi_df(self, kpi_df) -> pd.DataFrame:
152    self._check_valid()
153    psi_type = self._dropped_below_threshold_type
154    psi_threshold = self._dropped_below_threshold_value
155    kpi_df.loc[len(kpi_df)] = [CUJ_COMPLETED_EVENT, PSI_EVENT_TYPE, PSI_MONITOR_TAG,
156      float(self._cuj_completed_millis/1000.0), 'Seconds since monitor start']
157    kpi_df.loc[len(kpi_df)] = [
158      'Time taken to reach \'{}\' PSI baseline (including baseline calculation duration)'.format(
159        psi_type), PSI_EVENT_TYPE, PSI_MONITOR_TAG,
160      float(self._reached_baseline_millis - self._cuj_completed_millis)/1000.0, 'Duration seconds']
161    kpi_df.loc[len(kpi_df)] = [
162      'Total number of \'{}\' PSI entries used in baseline calculation'.format(psi_type),
163      PSI_EVENT_TYPE, PSI_MONITOR_TAG, self._reached_baseline_latest_psi_entries,
164      'Number of entries']
165    kpi_df.loc[len(kpi_df)] = ['Baseline calculation duration', PSI_MONITOR_TAG, PSI_EVENT_TYPE,
166      float(self._reached_baseline_total_duration_millis/1000.0), 'Duration seconds']
167    kpi_df.loc[len(kpi_df)] = ['Max \'{}\' PSI value'.format(psi_type), PSI_EVENT_TYPE,
168      PSI_MONITOR_TAG, self._max_psi_value, 'Percent']
169    kpi_df.loc[len(kpi_df)] = ['Time spent above the \'{}\' PSI threshold {}%'.format(psi_type,
170      psi_threshold), PSI_EVENT_TYPE, PSI_MONITOR_TAG,
171      float(self._dropped_below_threshold_millis - self._exceeded_threshold_millis)/1000.0,
172      'Duration seconds']
173    kpi_df.loc[len(kpi_df)] = ['Time taken to drop below the \'{}\' PSI threshold'.format(psi_type),
174      PSI_EVENT_TYPE, PSI_MONITOR_TAG,
175      (self._dropped_below_threshold_millis - self._cuj_completed_millis)/1000.0,
176      'Duration seconds']
177    kpi_df.loc[len(kpi_df)] = [
178      'Max \'{}\' PSI value after dropped below threshold'.format(psi_type), PSI_EVENT_TYPE,
179      PSI_MONITOR_TAG, self._max_psi_value_after_dropped_below_threshold, 'Percent']
180    kpi_df.loc[len(kpi_df)] = ['Total time spent above the \'{}\' PSI threshold 80%'.format(
181      psi_type), PSI_EVENT_TYPE, PSI_MONITOR_TAG,
182      float(self._total_duration_above_80p_threshold_millis)/1000.0, 'Duration seconds']
183    kpi_df.loc[len(kpi_df)] = ['Total time spent above the \'{}\' PSI threshold 50%'.format(
184      psi_type), PSI_EVENT_TYPE, PSI_MONITOR_TAG,
185      float(self._total_duration_above_50p_threshold_millis)/1000.0, 'Duration seconds']
186    kpi_df.loc[len(kpi_df)] = ['Total time spent above the \'{}\' PSI threshold 30%'.format(
187      psi_type), PSI_EVENT_TYPE, PSI_MONITOR_TAG,
188      float(self._total_duration_above_30p_threshold_millis)/1000.0, 'Duration seconds']
189    return kpi_df
190
191
192def process_events_df(events_df, kpi_df) -> pd.DataFrame:
193  if events_df.empty:
194    return kpi_df
195  filtered_events_df = events_df.dropna(subset=['event_key', 'duration_value'])
196  filtered_events_df = filtered_events_df[filtered_events_df['duration_value'] > 0]
197  for index, row in filtered_events_df.iterrows():
198    if row['event_key'] is None:
199      continue
200    kpi_df.loc[len(kpi_df)] = [row['event_key'], LOGCAT_EVENT_TYPE, row['event_tag'],
201      row['duration_value'], LOGCAT_EVENT_UNIT]
202  return kpi_df
203
204
205def get_kpi_df(psi_df, events_df) -> pd.DataFrame:
206  kpi_df = pd.DataFrame(columns=COLUMNS)
207  psi_kpi = PsiKpi(PSI_TO_MONITOR)
208  psi_kpi.process_psi_df(psi_df)
209  kpi_df = psi_kpi.populate_kpi_df(kpi_df)
210
211  if events_df is None:
212    return kpi_df
213
214  return process_events_df(events_df, kpi_df)
215
216
217def main():
218  parser = argparse.ArgumentParser(
219      description='Plot PSI csv dump from PSI monitor.',
220  )
221  parser.add_argument('--psi_csv', action='store', type=str, required=True, dest='psi_csv',
222                      help='PSI csv dump from psi_monitor.sh')
223  parser.add_argument('--events_csv', action='store', type=str, dest='events_csv',
224                      help='Events csv dump from run_cuj.sh')
225  parser.add_argument('--out_kpi_csv', action='store', type=str, dest='kpi_csv',
226                      default='kpis.csv', help='Output KPI CSV file')
227  args = parser.parse_args()
228
229  psi_df = pd.read_csv(args.psi_csv, converters={'event_descriptions': pd.eval})
230  events_df = None
231  if args.events_csv:
232    events_df = pd.read_csv(args.events_csv).drop_duplicates()
233
234  kpi_df = get_kpi_df(psi_df, events_df)
235
236  kpi_df.to_csv(args.kpi_csv, index=False)
237
238if __name__ == '__main__':
239  main()
240