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