Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 """Pulls historical swarming task metadata from Findit and prints a report.""" | |
| 6 | |
| 7 from collections import defaultdict | |
| 8 from collections import OrderedDict | |
| 9 import datetime | |
| 10 import os | |
| 11 import sys | |
| 12 | |
| 13 _REMOTE_API_DIR = os.path.join(os.path.dirname(__file__), os.path.pardir) | |
| 14 sys.path.insert(1, _REMOTE_API_DIR) | |
| 15 | |
| 16 import remote_api | |
| 17 | |
| 18 from model.wf_swarming_task import WfSwarmingTask | |
| 19 | |
| 20 | |
| 21 NOT_AVAILABLE = 'N/A' | |
| 22 | |
| 23 | |
| 24 # TODO(lijeffrey): Refactor helper methods into module sharable with | |
| 25 # try_job_data_query.py. | |
| 26 def _GetAverageOfNumbersInList(numbers): | |
| 27 """Returns a float average of numbers or NOT_AVAILABLE if numbers is empty.""" | |
| 28 return (float(sum(numbers)) / len(numbers)) if numbers else NOT_AVAILABLE | |
| 29 | |
| 30 | |
| 31 def _FormatDigits(number): | |
| 32 """Formats number into a 2-digit float, or NOT_AVAILABLE.""" | |
| 33 if isinstance(number, float): | |
| 34 return float('%.2f' % number) | |
| 35 return NOT_AVAILABLE | |
| 36 | |
| 37 | |
| 38 def _FormatSecondsAsHMS(seconds): | |
| 39 """Formats the number of seconds into hours, minutes, seconds.""" | |
| 40 if seconds == NOT_AVAILABLE: | |
| 41 return NOT_AVAILABLE | |
| 42 | |
| 43 minutes, seconds = divmod(seconds, 60) | |
| 44 hours, minutes = divmod(minutes, 60) | |
| 45 return '%d:%02d:%02d' % (hours, minutes, seconds) | |
| 46 | |
| 47 | |
| 48 def _FormatStepName(step_name): | |
| 49 # Formats step_name to return only the first word (the step name itself). | |
| 50 # Step names are expected to be in either the format 'step_name' or | |
| 51 # 'step_name on platform'. | |
| 52 return step_name.strip().split(' ')[0] | |
| 53 | |
| 54 | |
| 55 def _CategorizeSwarmingTaskData(swarming_task_list): | |
| 56 """Categorizes swarming_task_list into a dict. | |
| 57 | |
| 58 Args: | |
| 59 swarming_task_list: A list of WfSwarmingTask objects. | |
| 60 | |
| 61 Returns: | |
| 62 A dict in the format: | |
| 63 { | |
| 64 priority1: { | |
| 65 master_name1': { | |
| 66 'builder_name1': { | |
| 67 'step_name1': [WfSwarmingTask1, WfSwarmingTask2, ...], | |
| 68 ... | |
| 69 }, | |
| 70 ... | |
| 71 }, | |
| 72 ... | |
| 73 }, | |
| 74 ... | |
| 75 } | |
| 76 """ | |
| 77 categorized_data = defaultdict( | |
| 78 lambda: defaultdict( | |
| 79 lambda: defaultdict( | |
| 80 lambda: defaultdict(list)))) | |
| 81 | |
| 82 for swarming_task in swarming_task_list: | |
| 83 if not swarming_task.parameters or not swarming_task.tests_statuses: | |
| 84 # Disregard any swarming tasks that were triggered before 'parameters' and | |
| 85 # 'tests_statuses' were introduced. | |
| 86 continue | |
| 87 | |
| 88 priority = swarming_task.parameters['priority'] | |
| 89 master_name = swarming_task.master_name | |
| 90 builder_name = swarming_task.builder_name | |
| 91 step_name = swarming_task.key.id() | |
| 92 | |
| 93 categorized_data[priority][master_name][builder_name][step_name].append( | |
| 94 swarming_task) | |
| 95 | |
| 96 return categorized_data | |
| 97 | |
| 98 | |
| 99 def _GetReportInformation(swarming_task_list, start_date, end_date): | |
| 100 """Computes and returns swarming task metadata in a dict. | |
| 101 | |
| 102 Args: | |
| 103 swarming_task_list: A list of WfSwarmingTask entities. | |
| 104 start_date: The earliest request date to compute data. | |
| 105 end_date: The latest request date to compute data. | |
| 106 | |
| 107 Returns: | |
| 108 A dict in the following format: | |
| 109 { | |
| 110 'swarming_tasks_per_day': The average number of swwarming tasks | |
| 111 requested over the time period specified, | |
| 112 'average_execution_time': The average amount of time spent on each | |
| 113 swarming task not including in-queue time. | |
| 114 'average_time_in_queue': The average amount of time a swarming task | |
| 115 spends in-queue before it is picked up. | |
| 116 'longest_execution_time': The length of time of the slowest swarming | |
| 117 task in the period requested, | |
| 118 'shortest_execution_time': The length of time of the fastest swarming | |
| 119 task in the period requested. | |
| 120 'tests_times_iterations': The number of tests multiplied by the number | |
| 121 of iterations that test was run. | |
| 122 'average_number_of_iterations': The average number of iterations each | |
| 123 test for this step was run. | |
| 124 } | |
| 125 """ | |
| 126 swarming_tasks_per_day = NOT_AVAILABLE | |
| 127 average_execution_time = NOT_AVAILABLE | |
| 128 average_time_in_queue = NOT_AVAILABLE | |
| 129 longest_execution_time = NOT_AVAILABLE | |
| 130 shortest_execution_time = NOT_AVAILABLE | |
| 131 average_number_of_iterations = NOT_AVAILABLE | |
| 132 average_number_of_tests_run = NOT_AVAILABLE | |
| 133 | |
| 134 if swarming_task_list: | |
| 135 swarming_tasks_per_day = ( | |
| 136 len(swarming_task_list) / float((end_date - start_date).days)) | |
| 137 execution_times_seconds = [] | |
| 138 in_queue_times = [] | |
| 139 iteration_counts = [] | |
| 140 tests_counts = [] | |
| 141 | |
| 142 for swarming_task in swarming_task_list: | |
| 143 # Execution time. | |
| 144 if swarming_task.started_time and swarming_task.completed_time: | |
| 145 execution_times_seconds.append( | |
| 146 (swarming_task.completed_time - swarming_task.started_time).seconds) | |
| 147 | |
| 148 # In-queue time. | |
| 149 if swarming_task.started_time and swarming_task.created_time: | |
| 150 in_queue_times.append( | |
| 151 (swarming_task.started_time - swarming_task.created_time).seconds) | |
| 152 | |
| 153 # Number of iterations. | |
| 154 iterations_to_rerun = swarming_task.parameters.get( | |
| 155 'iterations_to_rerun') | |
| 156 if iterations_to_rerun is not None: | |
| 157 iteration_counts.append(iterations_to_rerun) | |
| 158 | |
| 159 # Number of tests. | |
| 160 number_of_tests = len(swarming_task.tests_statuses) | |
| 161 if number_of_tests: | |
| 162 tests_counts.append(number_of_tests) | |
| 163 | |
| 164 average_execution_time = (_GetAverageOfNumbersInList( | |
| 165 execution_times_seconds) if execution_times_seconds else NOT_AVAILABLE) | |
| 166 average_time_in_queue = ( | |
| 167 _GetAverageOfNumbersInList(in_queue_times) if in_queue_times else | |
| 168 NOT_AVAILABLE) | |
| 169 longest_execution_time = ( | |
| 170 str(datetime.timedelta(seconds=max(execution_times_seconds))) | |
| 171 if execution_times_seconds else NOT_AVAILABLE) | |
| 172 shortest_execution_time = ( | |
| 173 str(datetime.timedelta(seconds=min(execution_times_seconds))) | |
| 174 if execution_times_seconds else NOT_AVAILABLE) | |
| 175 average_number_of_iterations = _GetAverageOfNumbersInList(iteration_counts) | |
| 176 average_number_of_tests_run = _GetAverageOfNumbersInList(tests_counts) | |
| 177 tests_times_iterations = ( | |
| 178 average_number_of_iterations * average_number_of_tests_run) | |
| 179 | |
| 180 return { | |
| 181 'swarming_tasks_per_day': swarming_tasks_per_day, | |
| 182 'average_execution_time': average_execution_time, | |
| 183 'average_time_in_queue': average_time_in_queue, | |
| 184 'longest_execution_time': longest_execution_time, | |
| 185 'shortest_execution_time': shortest_execution_time, | |
| 186 'tests_times_iterations': tests_times_iterations, | |
| 187 'average_number_of_iterations': average_number_of_iterations, | |
| 188 'average_number_of_tests_run': average_number_of_tests_run, | |
| 189 } | |
| 190 | |
| 191 | |
| 192 def _GetReport(categorized_swarming_task_dict, start_date, end_date): | |
| 193 """Returns a swarming task data report as an ordered dict sorted by priority. | |
| 194 | |
| 195 Args: | |
| 196 categorized_swarming_task_dict: A dict categorizing WFSwarmingTask entities | |
| 197 organized by priority, master_name, builder_name, step_name. This dict | |
| 198 should be the output from _CategorizeSwarmingTaskData(). | |
| 199 start_date: The earliest request date for which data should be computed. | |
| 200 end_date: The latest request date for which data should be computed. | |
| 201 | |
| 202 Returns: | |
| 203 An ordered dict by highest priority (lower priority number) swarming tasks | |
| 204 in the format: | |
| 205 { | |
| 206 priority: { | |
| 207 'master_name': { | |
| 208 'builder_name': { | |
| 209 'step_name': { | |
| 210 'swarming_tasks_per_day': number or 'N/A', | |
| 211 'average_execution_time': number or 'N/A', | |
| 212 'average_time_in_queue': number or 'N/A', | |
| 213 'longest_execution_time': number or 'N/A', | |
| 214 'shortest_execution_time': number or 'N/A', | |
| 215 'tests_times_iterations': number or 'N/A' | |
| 216 'average_number_of_tests_run': number or 'N/A', | |
| 217 'average_number_of_iterations': number or 'N/A', | |
| 218 }, | |
| 219 ... | |
| 220 }, | |
| 221 ... | |
| 222 }, | |
| 223 ... | |
| 224 }, | |
| 225 ... | |
| 226 } | |
| 227 """ | |
| 228 report = {} | |
| 229 | |
| 230 for priority, masters in categorized_swarming_task_dict.iteritems(): | |
| 231 report[priority] = {} | |
| 232 | |
| 233 for master, builders in masters.iteritems(): | |
| 234 report[priority][master] = {} | |
| 235 | |
| 236 for builder, steps in builders.iteritems(): | |
| 237 report[priority][master][builder] = {} | |
| 238 | |
| 239 for step, swarming_task_data_list in steps.iteritems(): | |
|
chanli
2016/03/08 23:31:39
nit: Since the layers are fixed, maybe you can use
lijeffrey
2016/03/09 18:36:52
Done.
| |
| 240 report[priority][master][builder][step] = _GetReportInformation( | |
| 241 swarming_task_data_list, start_date, end_date) | |
| 242 | |
| 243 return OrderedDict(sorted(report.items())) | |
| 244 | |
| 245 | |
| 246 def CreateHtmlPage(report, start_date, end_date): | |
| 247 """Generates an html string for displaying the report. | |
| 248 | |
| 249 Args: | |
| 250 report: A dict containing all the relevant information returned from | |
| 251 _GetReport(). | |
| 252 start_date: The earliest date that a swarming task was requested. | |
| 253 end_date: The latest date that a swarming task was requested. | |
| 254 | |
| 255 Returns: | |
| 256 A string containing the html body for the final report page. | |
| 257 """ | |
| 258 html = """ | |
| 259 <style> | |
| 260 table { | |
| 261 border-collapse: collapse; | |
| 262 border: 1px solid gray; | |
| 263 } | |
| 264 table td, th { | |
| 265 border: 1px solid gray; | |
| 266 } | |
| 267 </style>""" | |
| 268 html += '<b>Swarming task metadata from %s to %s (%s days)</b>' % ( | |
| 269 str(start_date), str(end_date), (end_date - start_date).days) | |
| 270 html += '<h1>Aggregate metadata for swarming tasks by priority</h1>' | |
| 271 | |
| 272 cell_template = '<td>%s</td>' | |
| 273 | |
| 274 for priority, masters in report.iteritems(): | |
| 275 html += '<h2>Task Priority: %s</h2>' % priority | |
| 276 html += """ | |
| 277 <table> | |
| 278 <tr> | |
| 279 <th>Master</th> | |
| 280 <th>Builder</th> | |
| 281 <th>Step</th> | |
| 282 <th>Average # Tasks Per Day</th> | |
| 283 <th>Average Time In Queue</th> | |
| 284 <th>Average Execution Time</th> | |
| 285 <th>Longest Execution Time</th> | |
| 286 <th>Shortest Execution Time</th> | |
| 287 <th># Tests * # Iterations</th> | |
| 288 <th>Average # Iterations</th> | |
| 289 <th>Average # Tests Run</th> | |
| 290 </tr>""" | |
| 291 | |
| 292 for master_name, builder_reports in masters.iteritems(): | |
| 293 for builder_name, steps in builder_reports.iteritems(): | |
| 294 for step_name in steps: | |
| 295 builder_report = ( | |
| 296 report[priority][master_name][builder_name][step_name]) | |
| 297 | |
| 298 html += '<tr>' | |
| 299 html += cell_template % master_name | |
| 300 html += cell_template % builder_name | |
| 301 html += cell_template % _FormatStepName(step_name) | |
| 302 html += cell_template % _FormatDigits( | |
| 303 builder_report['swarming_tasks_per_day']) | |
| 304 html += cell_template % _FormatSecondsAsHMS(_FormatDigits( | |
| 305 builder_report['average_time_in_queue'])) | |
| 306 html += cell_template % _FormatSecondsAsHMS(_FormatDigits( | |
| 307 builder_report['average_execution_time'])) | |
| 308 html += cell_template % builder_report['longest_execution_time'] | |
| 309 html += cell_template % builder_report['shortest_execution_time'] | |
| 310 html += cell_template % _FormatDigits( | |
| 311 builder_report['tests_times_iterations']) | |
| 312 html += cell_template % _FormatDigits( | |
| 313 builder_report['average_number_of_iterations']) | |
| 314 html += cell_template % _FormatDigits( | |
| 315 builder_report['average_number_of_tests_run']) | |
| 316 | |
| 317 html += '</table>' | |
| 318 | |
| 319 return html | |
| 320 | |
| 321 | |
| 322 if __name__ == '__main__': | |
| 323 # Set up the Remote API to use services on the live App Engine. | |
| 324 remote_api.EnableRemoteApi(app_id='findit-for-me') | |
| 325 | |
| 326 START_DATE = datetime.datetime(2016, 2, 1) | |
| 327 END_DATE = datetime.datetime(2016, 3, 7) | |
| 328 | |
| 329 wf_analysis_query = WfSwarmingTask.query( | |
| 330 WfSwarmingTask.created_time >= START_DATE, | |
| 331 WfSwarmingTask.created_time < END_DATE) | |
| 332 data_list = wf_analysis_query.fetch() | |
| 333 | |
| 334 categorized_data_dict = _CategorizeSwarmingTaskData(data_list) | |
| 335 final_report = _GetReport(categorized_data_dict, START_DATE, END_DATE) | |
| 336 | |
| 337 findit_tmp_dir = os.environ.get('TMP_DIR') | |
| 338 if not findit_tmp_dir: | |
| 339 findit_tmp_dir = os.getcwd() | |
| 340 | |
| 341 report_path = os.path.join(findit_tmp_dir, | |
| 342 'swarming_task_metadata_report.html') | |
| 343 | |
| 344 with open(report_path, 'w') as f: | |
| 345 f.write(CreateHtmlPage(final_report, START_DATE, END_DATE)) | |
| 346 | |
| 347 print 'Swarming task metadata report available at file://%s' % report_path | |
| OLD | NEW |