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