1# Copyright 2017 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"""URL endpoint containing server-side functionality for pinpoint jobs.""" 5from __future__ import print_function 6from __future__ import division 7from __future__ import absolute_import 8 9import json 10import logging 11 12from google.appengine.ext import ndb 13 14from dashboard import find_change_points 15from dashboard.common import descriptor 16from dashboard.common import math_utils 17from dashboard.common import request_handler 18from dashboard.common import utils 19from dashboard.models import anomaly 20from dashboard.models import anomaly_config 21from dashboard.models import graph_data 22from dashboard.services import crrev_service 23from dashboard.services import pinpoint_service 24 25_NON_CHROME_TARGETS = ['v8'] 26_SUITE_CRREV_CONFIGS = { 27 'v8': ['chromium', 'v8/v8'], 28 'webrtc_perf_tests': ['webrtc', 'src'], 29} 30 31 32class InvalidParamsError(Exception): 33 pass 34 35 36class PinpointNewPrefillRequestHandler(request_handler.RequestHandler): 37 38 def post(self): 39 t = utils.TestKey(self.request.get('test_path')).get() 40 self.response.write(json.dumps({'story_filter': t.unescaped_story_name})) 41 42 43class PinpointNewBisectRequestHandler(request_handler.RequestHandler): 44 45 def post(self): 46 job_params = dict( 47 (a, self.request.get(a)) for a in self.request.arguments()) 48 self.response.write(json.dumps(NewPinpointBisect(job_params))) 49 50 51def NewPinpointBisect(job_params): 52 logging.info('Job Params: %s', job_params) 53 54 try: 55 pinpoint_params = PinpointParamsFromBisectParams(job_params) 56 logging.info('Pinpoint Params: %s', pinpoint_params) 57 except InvalidParamsError as e: 58 return {'error': e.message} 59 60 results = pinpoint_service.NewJob(pinpoint_params) 61 logging.info('Pinpoint Service Response: %s', results) 62 63 alert_keys = job_params.get('alerts') 64 if 'jobId' in results and alert_keys: 65 alerts = json.loads(alert_keys) 66 for alert_urlsafe_key in alerts: 67 alert = ndb.Key(urlsafe=alert_urlsafe_key).get() 68 alert.pinpoint_bisects.append(results['jobId']) 69 alert.put() 70 71 return results 72 73 74class PinpointNewPerfTryRequestHandler(request_handler.RequestHandler): 75 76 def post(self): 77 job_params = dict( 78 (a, self.request.get(a)) for a in self.request.arguments()) 79 80 try: 81 pinpoint_params = PinpointParamsFromPerfTryParams(job_params) 82 except InvalidParamsError as e: 83 self.response.write(json.dumps({'error': e.message})) 84 return 85 86 self.response.write(json.dumps(pinpoint_service.NewJob(pinpoint_params))) 87 88 89def _GitHashToCommitPosition(commit_position): 90 try: 91 commit_position = int(commit_position) 92 except ValueError: 93 result = crrev_service.GetCommit(commit_position) 94 if 'error' in result: 95 raise InvalidParamsError('Error retrieving commit info: %s' % 96 result['error'].get('message')) 97 commit_position = int(result['number']) 98 return commit_position 99 100 101def FindMagnitudeBetweenCommits(test_key, start_commit, end_commit): 102 start_commit = _GitHashToCommitPosition(start_commit) 103 end_commit = _GitHashToCommitPosition(end_commit) 104 105 test = test_key.get() 106 num_points = anomaly_config.GetAnomalyConfigDict(test).get( 107 'min_segment_size', find_change_points.MIN_SEGMENT_SIZE) 108 start_rows = graph_data.GetRowsForTestBeforeAfterRev(test_key, start_commit, 109 num_points, 0) 110 end_rows = graph_data.GetRowsForTestBeforeAfterRev(test_key, end_commit, 0, 111 num_points) 112 113 if not start_rows or not end_rows: 114 return None 115 116 median_before = math_utils.Median([r.value for r in start_rows]) 117 median_after = math_utils.Median([r.value for r in end_rows]) 118 119 return median_after - median_before 120 121 122def ResolveToGitHash(commit_position, suite, crrev=None): 123 crrev = crrev or crrev_service 124 try: 125 int(commit_position) 126 if suite in _SUITE_CRREV_CONFIGS: 127 project, repo = _SUITE_CRREV_CONFIGS[suite] 128 else: 129 project, repo = 'chromium', 'chromium/src' 130 result = crrev.GetNumbering( 131 number=commit_position, 132 numbering_identifier='refs/heads/master', 133 numbering_type='COMMIT_POSITION', 134 project=project, 135 repo=repo) 136 if 'error' in result: 137 raise InvalidParamsError('Error retrieving commit info: %s' % 138 result['error'].get('message')) 139 return result['git_sha'] 140 except ValueError: 141 pass 142 143 # It was probably a git hash, so just return as is 144 return commit_position 145 146 147def GetIsolateTarget(bot_name, suite): 148 if suite in _NON_CHROME_TARGETS: 149 return '' 150 151 # ChromeVR 152 if suite.startswith('xr.'): 153 return 'vr_perf_tests' 154 155 # WebRTC perf tests 156 if suite == 'webrtc_perf_tests': 157 return 'webrtc_perf_tests' 158 159 # This is a special-case for webview, which we probably don't need to handle 160 # in the Dashboard (instead should just support in Pinpoint through 161 # configuration). 162 if 'webview' in bot_name.lower(): 163 return 'performance_webview_test_suite' 164 return 'performance_test_suite' 165 166 167def ParseGroupingLabelChartNameAndTraceName(test_path): 168 """Returns grouping_label, chart_name, trace_name from a test path.""" 169 test_path_parts = test_path.split('/') 170 suite = test_path_parts[2] 171 if suite in _NON_CHROME_TARGETS: 172 return '', '', '' 173 174 test = ndb.Key('TestMetadata', '/'.join(test_path_parts)).get() 175 grouping_label, chart_name, trace_name = utils.ParseTelemetryMetricParts( 176 test_path) 177 if trace_name and test.unescaped_story_name: 178 trace_name = test.unescaped_story_name 179 return grouping_label, chart_name, trace_name 180 181 182def ParseStatisticNameFromChart(chart_name): 183 chart_name_parts = chart_name.split('_') 184 statistic_name = '' 185 186 if chart_name_parts[-1] in descriptor.STATISTICS: 187 chart_name = '_'.join(chart_name_parts[:-1]) 188 statistic_name = chart_name_parts[-1] 189 return chart_name, statistic_name 190 191 192def PinpointParamsFromPerfTryParams(params): 193 """Takes parameters from Dashboard's pinpoint-perf-job-dialog and returns 194 a dict with parameters for a new Pinpoint job. 195 196 Args: 197 params: A dict in the following format: 198 { 199 'test_path': Test path for the metric being bisected. 200 'start_commit': Git hash or commit position of earlier revision. 201 'end_commit': Git hash or commit position of later revision. 202 'extra_test_args': Extra args for the swarming job. 203 } 204 205 Returns: 206 A dict of params for passing to Pinpoint to start a job, or a dict with an 207 'error' field. 208 """ 209 if not utils.IsValidSheriffUser(): 210 user = utils.GetEmail() 211 raise InvalidParamsError('User "%s" not authorized.' % user) 212 213 test_path = params['test_path'] 214 test_path_parts = test_path.split('/') 215 bot_name = test_path_parts[1] 216 suite = test_path_parts[2] 217 218 start_commit = params['start_commit'] 219 end_commit = params['end_commit'] 220 start_git_hash = ResolveToGitHash(start_commit, suite) 221 end_git_hash = ResolveToGitHash(end_commit, suite) 222 story_filter = params['story_filter'] 223 224 # Pinpoint also requires you specify which isolate target to run the 225 # test, so we derive that from the suite name. Eventually, this would 226 # ideally be stored in a SparseDiagnostic but for now we can guess. 227 target = GetIsolateTarget(bot_name, suite) 228 229 extra_test_args = params.get('extra_test_args') 230 231 email = utils.GetEmail() 232 job_name = 'Try job on %s/%s' % (bot_name, suite) 233 234 pinpoint_params = { 235 'comparison_mode': 'try', 236 'configuration': bot_name, 237 'benchmark': suite, 238 'base_git_hash': start_git_hash, 239 'end_git_hash': end_git_hash, 240 'extra_test_args': extra_test_args, 241 'target': target, 242 'user': email, 243 'name': job_name 244 } 245 246 if story_filter: 247 pinpoint_params['story'] = story_filter 248 249 return pinpoint_params 250 251 252def PinpointParamsFromBisectParams(params): 253 """Takes parameters from Dashboard's pinpoint-job-dialog and returns 254 a dict with parameters for a new Pinpoint job. 255 256 Args: 257 params: A dict in the following format: 258 { 259 'test_path': Test path for the metric being bisected. 260 'start_git_hash': Git hash of earlier revision. 261 'end_git_hash': Git hash of later revision. 262 'bug_id': Associated bug. 263 'project_id': Associated Monorail project. 264 } 265 266 Returns: 267 A dict of params for passing to Pinpoint to start a job, or a dict with an 268 'error' field. 269 """ 270 if not utils.IsValidSheriffUser(): 271 user = utils.GetEmail() 272 raise InvalidParamsError('User "%s" not authorized.' % user) 273 274 test_path = params['test_path'] 275 test_path_parts = test_path.split('/') 276 bot_name = test_path_parts[1] 277 suite = test_path_parts[2] 278 279 # If functional bisects are speciied, Pinpoint expects these parameters to be 280 # empty. 281 bisect_mode = params['bisect_mode'] 282 if bisect_mode != 'performance' and bisect_mode != 'functional': 283 raise InvalidParamsError('Invalid bisect mode %s specified.' % bisect_mode) 284 285 start_commit = params['start_commit'] 286 end_commit = params['end_commit'] 287 start_git_hash = ResolveToGitHash(start_commit, suite) 288 end_git_hash = ResolveToGitHash(end_commit, suite) 289 290 # Pinpoint also requires you specify which isolate target to run the 291 # test, so we derive that from the suite name. Eventually, this would 292 # ideally be stored in a SparesDiagnostic but for now we can guess. 293 target = GetIsolateTarget(bot_name, suite) 294 295 email = utils.GetEmail() 296 job_name = '%s bisect on %s/%s' % (bisect_mode.capitalize(), bot_name, suite) 297 298 alert_key = '' 299 if params.get('alerts'): 300 alert_keys = json.loads(params.get('alerts')) 301 if alert_keys: 302 alert_key = alert_keys[0] 303 304 alert_magnitude = None 305 if alert_key: 306 alert = ndb.Key(urlsafe=alert_key).get() 307 alert_magnitude = alert.median_after_anomaly - alert.median_before_anomaly 308 309 if not alert_magnitude: 310 alert_magnitude = FindMagnitudeBetweenCommits( 311 utils.TestKey(test_path), start_commit, end_commit) 312 313 if isinstance(params['bug_id'], int): 314 issue_id = params['bug_id'] if params['bug_id'] > 0 else None 315 else: 316 issue_id = int(params['bug_id']) if params['bug_id'].isdigit() else None 317 issue = anomaly.Issue( 318 project_id=params.get('project_id', 'chromium'), issue_id=issue_id) 319 320 return pinpoint_service.MakeBisectionRequest( 321 test=utils.TestKey(test_path).get(), 322 commit_range=pinpoint_service.CommitRange( 323 start=start_git_hash, end=end_git_hash), 324 issue=issue, 325 comparison_mode=bisect_mode, 326 target=target, 327 comparison_magnitude=alert_magnitude, 328 user=email, 329 name=job_name, 330 story_filter=params['story_filter'], 331 pin=params.get('pin'), 332 tags={ 333 'test_path': test_path, 334 'alert': alert_key, 335 }, 336 ) 337