blob: bca641a0aeb3193ef951fc95eaf869be50c03541 [file] [log] [blame]
Patrik Höglund0569a122020-03-13 12:26:42 +01001#!/usr/bin/env python
2# Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
3#
4# Use of this source code is governed by a BSD-style license
5# that can be found in the LICENSE file in the root of the source
6# tree. An additional intellectual property rights grant can be found
7# in the file PATENTS. All contributing project authors may
8# be found in the AUTHORS file in the root of the source tree.
9
Andrey Logvin728b5d02020-11-11 17:16:26 +000010import datetime
Patrik Höglund0569a122020-03-13 12:26:42 +010011import httplib2
12import json
Andrey Logvin9db3ab22020-11-25 09:47:40 +000013import math
Patrik Höglund0569a122020-03-13 12:26:42 +010014import subprocess
Andrey Logvin728b5d02020-11-11 17:16:26 +000015import time
Patrik Höglund0569a122020-03-13 12:26:42 +010016import zlib
17
Patrik Höglund620bed12020-03-17 09:59:10 +010018from tracing.value import histogram
Patrik Höglund0569a122020-03-13 12:26:42 +010019from tracing.value import histogram_set
20from tracing.value.diagnostics import generic_set
21from tracing.value.diagnostics import reserved_infos
22
23
24def _GenerateOauthToken():
Mirko Bonadei8cc66952020-10-30 10:13:45 +010025 args = ['luci-auth', 'token']
26 p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
27 if p.wait() == 0:
28 output = p.stdout.read()
29 return output.strip()
30 else:
31 raise RuntimeError(
32 'Error generating authentication token.\nStdout: %s\nStderr:%s' %
33 (p.stdout.read(), p.stderr.read()))
Patrik Höglund0569a122020-03-13 12:26:42 +010034
35
Andrey Logvinbce02a92020-11-24 10:04:50 +000036def _CreateHeaders(oauth_token):
37 return {'Authorization': 'Bearer %s' % oauth_token}
38
39
40def _SendHistogramSet(url, histograms):
Mirko Bonadei8cc66952020-10-30 10:13:45 +010041 """Make a HTTP POST with the given JSON to the Performance Dashboard.
Patrik Höglund0569a122020-03-13 12:26:42 +010042
Andrey Logvin728b5d02020-11-11 17:16:26 +000043 Args:
44 url: URL of Performance Dashboard instance, e.g.
45 "https://chromeperf.appspot.com".
46 histograms: a histogram set object that contains the data to be sent.
Andrey Logvin728b5d02020-11-11 17:16:26 +000047 """
Andrey Logvinbce02a92020-11-24 10:04:50 +000048 headers = _CreateHeaders(_GenerateOauthToken())
Patrik Höglund0569a122020-03-13 12:26:42 +010049
Mirko Bonadei8cc66952020-10-30 10:13:45 +010050 serialized = json.dumps(_ApplyHacks(histograms.AsDicts()), indent=4)
Patrik Höglund0569a122020-03-13 12:26:42 +010051
Mirko Bonadei8cc66952020-10-30 10:13:45 +010052 if url.startswith('http://localhost'):
53 # The catapult server turns off compression in developer mode.
54 data = serialized
55 else:
56 data = zlib.compress(serialized)
Patrik Höglund0569a122020-03-13 12:26:42 +010057
Mirko Bonadei8cc66952020-10-30 10:13:45 +010058 print 'Sending %d bytes to %s.' % (len(data), url + '/add_histograms')
Patrik Höglund0569a122020-03-13 12:26:42 +010059
Mirko Bonadei8cc66952020-10-30 10:13:45 +010060 http = httplib2.Http()
61 response, content = http.request(url + '/add_histograms',
62 method='POST',
63 body=data,
64 headers=headers)
65 return response, content
Patrik Höglund0569a122020-03-13 12:26:42 +010066
67
Andrey Logvinbce02a92020-11-24 10:04:50 +000068def _WaitForUploadConfirmation(url, upload_token, wait_timeout,
Andrey Logvin728b5d02020-11-11 17:16:26 +000069 wait_polling_period):
70 """Make a HTTP GET requests to the Performance Dashboard untill upload
71 status is known or the time is out.
72
73 Args:
74 url: URL of Performance Dashboard instance, e.g.
75 "https://chromeperf.appspot.com".
Andrey Logvin728b5d02020-11-11 17:16:26 +000076 upload_token: String that identifies Performance Dashboard and can be used
77 for the status check.
78 wait_timeout: (datetime.timedelta) Maximum time to wait for the
79 confirmation.
80 wait_polling_period: (datetime.timedelta) Performance Dashboard will be
81 polled every wait_polling_period amount of time.
82 """
83 assert wait_polling_period <= wait_timeout
84
Andrey Logvinbce02a92020-11-24 10:04:50 +000085 headers = _CreateHeaders(_GenerateOauthToken())
Andrey Logvin728b5d02020-11-11 17:16:26 +000086 http = httplib2.Http()
87
Andrey Logvinbce02a92020-11-24 10:04:50 +000088 oauth_refreshed = False
Andrey Logvin728b5d02020-11-11 17:16:26 +000089 response = None
90 resp_json = None
91 current_time = datetime.datetime.now()
92 end_time = current_time + wait_timeout
93 next_poll_time = current_time + wait_polling_period
94 while datetime.datetime.now() < end_time:
95 current_time = datetime.datetime.now()
96 if next_poll_time > current_time:
97 time.sleep((next_poll_time - current_time).total_seconds())
98 next_poll_time = datetime.datetime.now() + wait_polling_period
99
Andrey Logvin9e302ea2020-11-18 16:59:57 +0000100 response, content = http.request(url + '/uploads/' + upload_token,
Andrey Logvin728b5d02020-11-11 17:16:26 +0000101 method='GET', headers=headers)
Andrey Logvine850af22020-11-18 15:23:53 +0000102
103 print 'Upload state polled. Response: %r.' % content
104
Andrey Logvinbce02a92020-11-24 10:04:50 +0000105 if not oauth_refreshed and response.status == 403:
106 print 'Oauth token refreshed. Continue polling.'
107 headers = _CreateHeaders(_GenerateOauthToken())
108 oauth_refreshed = True
109 continue
110
Andrey Logvin9e302ea2020-11-18 16:59:57 +0000111 if response.status != 200:
112 break
113
Andrey Logvin728b5d02020-11-11 17:16:26 +0000114 resp_json = json.loads(content)
Andrey Logvin9e302ea2020-11-18 16:59:57 +0000115 if resp_json['state'] == 'COMPLETED' or resp_json['state'] == 'FAILED':
Andrey Logvin728b5d02020-11-11 17:16:26 +0000116 break
117
118 return response, resp_json
119
120
Andrey Logvin844125c2020-11-18 22:07:15 +0000121# Because of an issues on the Dashboard side few measurements over a large set
122# can fail to upload. That would lead to the whole upload to be marked as
123# failed. Check it, so it doesn't increase flakiness of our tests.
124# TODO(crbug.com/1145904): Remove check after fixed.
Andrey Logvinbce02a92020-11-24 10:04:50 +0000125def _CheckFullUploadInfo(url, upload_token,
Andrey Logvin844125c2020-11-18 22:07:15 +0000126 min_measurements_amount=100,
127 max_failed_measurements_amount=1):
128 """Make a HTTP GET requests to the Performance Dashboard to get full info
129 about upload (including measurements). Checks if upload is correct despite
130 not having status "COMPLETED".
131
132 Args:
133 url: URL of Performance Dashboard instance, e.g.
134 "https://chromeperf.appspot.com".
Andrey Logvin844125c2020-11-18 22:07:15 +0000135 upload_token: String that identifies Performance Dashboard and can be used
136 for the status check.
137 min_measurements_amount: minimal amount of measurements that the upload
138 should have to start tolerating failures in particular measurements.
139 max_failed_measurements_amount: maximal amount of failured measurements to
140 tolerate.
141 """
Andrey Logvinbce02a92020-11-24 10:04:50 +0000142 headers = _CreateHeaders(_GenerateOauthToken())
Andrey Logvin844125c2020-11-18 22:07:15 +0000143 http = httplib2.Http()
144
145 response, content = http.request(url + '/uploads/' + upload_token +
146 '?additional_info=measurements',
147 method='GET', headers=headers)
148
149 print 'Full upload info: %r.' % content
150
151 if response.status != 200:
152 print 'Failed to reach the dashboard to get full upload info.'
153 return False
154
155 resp_json = json.loads(content)
156 if 'measurements' in resp_json:
157 measurements_cnt = len(resp_json['measurements'])
158 not_completed_state_cnt = len([
159 m for m in resp_json['measurements']
160 if m['state'] != 'COMPLETED'
161 ])
162
163 if (measurements_cnt >= min_measurements_amount and
164 not_completed_state_cnt <= max_failed_measurements_amount):
165 print('Not all measurements were uploaded. Measurements count: %d, '
166 'failed to upload: %d' %
167 (measurements_cnt, not_completed_state_cnt))
168 return True
169
170 return False
171
172
Patrik Höglund457c8cf2020-03-13 14:43:21 +0100173# TODO(https://crbug.com/1029452): HACKHACK
Andrey Logvin9db3ab22020-11-25 09:47:40 +0000174# Remove once we have doubles in the proto and handle -infinity and NaN
175# correctly.
Patrik Höglunda89ad612020-03-13 16:08:08 +0100176def _ApplyHacks(dicts):
Andrey Logvin659d7012020-11-24 15:12:25 +0000177 def _NoInf(value):
178 if value == float('inf'):
179 return histogram.JS_MAX_VALUE
180 if value == float('-inf'):
181 return -histogram.JS_MAX_VALUE
182 return value
183
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100184 for d in dicts:
185 if 'running' in d:
Andrey Logvin9db3ab22020-11-25 09:47:40 +0000186 d['running'] = [
187 _NoInf(value) for value in d['running']
188 if not math.isnan(value)]
Andrey Logvin659d7012020-11-24 15:12:25 +0000189 if 'sampleValues' in d:
Andrey Logvin9db3ab22020-11-25 09:47:40 +0000190 # We always have a single sample value. If it's NaN - the upload
191 # should fail.
Andrey Logvin659d7012020-11-24 15:12:25 +0000192 d['sampleValues'] = [_NoInf(value) for value in d['sampleValues']]
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100193
194 return dicts
Patrik Höglund457c8cf2020-03-13 14:43:21 +0100195
196
Patrik Höglund0569a122020-03-13 12:26:42 +0100197def _LoadHistogramSetFromProto(options):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100198 hs = histogram_set.HistogramSet()
199 with options.input_results_file as f:
200 hs.ImportProto(f.read())
Patrik Höglund0569a122020-03-13 12:26:42 +0100201
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100202 return hs
Patrik Höglund0569a122020-03-13 12:26:42 +0100203
204
205def _AddBuildInfo(histograms, options):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100206 common_diagnostics = {
207 reserved_infos.MASTERS: options.perf_dashboard_machine_group,
208 reserved_infos.BOTS: options.bot,
209 reserved_infos.POINT_ID: options.commit_position,
210 reserved_infos.BENCHMARKS: options.test_suite,
211 reserved_infos.WEBRTC_REVISIONS: str(options.webrtc_git_hash),
212 reserved_infos.BUILD_URLS: options.build_page_url,
213 }
Patrik Höglund0569a122020-03-13 12:26:42 +0100214
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100215 for k, v in common_diagnostics.items():
216 histograms.AddSharedDiagnosticToAllHistograms(
217 k.name, generic_set.GenericSet([v]))
Patrik Höglund0569a122020-03-13 12:26:42 +0100218
219
220def _DumpOutput(histograms, output_file):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100221 with output_file:
222 json.dump(_ApplyHacks(histograms.AsDicts()), output_file, indent=4)
Patrik Höglund0569a122020-03-13 12:26:42 +0100223
224
Patrik Höglund0569a122020-03-13 12:26:42 +0100225def UploadToDashboard(options):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100226 histograms = _LoadHistogramSetFromProto(options)
227 _AddBuildInfo(histograms, options)
Patrik Höglund0569a122020-03-13 12:26:42 +0100228
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100229 if options.output_json_file:
230 _DumpOutput(histograms, options.output_json_file)
Patrik Höglund0569a122020-03-13 12:26:42 +0100231
Andrey Logvinbce02a92020-11-24 10:04:50 +0000232 response, content = _SendHistogramSet(options.dashboard_url, histograms)
Patrik Höglund0569a122020-03-13 12:26:42 +0100233
Andrey Logvin728b5d02020-11-11 17:16:26 +0000234 upload_token = json.loads(content).get('token')
235 if not options.wait_for_upload or not upload_token:
236 print 'Not waiting for upload status confirmation.'
237 if response.status == 200:
238 print 'Received 200 from dashboard.'
239 return 0
240 else:
241 print('Upload failed with %d: %s\n\n%s' % (response.status,
242 response.reason, content))
243 return 1
244
245 response, resp_json = _WaitForUploadConfirmation(
246 options.dashboard_url,
Andrey Logvin728b5d02020-11-11 17:16:26 +0000247 upload_token,
248 datetime.timedelta(seconds=options.wait_timeout_sec),
249 datetime.timedelta(seconds=options.wait_polling_period_sec))
250
Andrey Logvin844125c2020-11-18 22:07:15 +0000251 if ((resp_json and resp_json['state'] == 'COMPLETED') or
Andrey Logvinbce02a92020-11-24 10:04:50 +0000252 _CheckFullUploadInfo(options.dashboard_url, upload_token)):
Andrey Logvin844125c2020-11-18 22:07:15 +0000253 print 'Upload completed.'
254 return 0
255
Andrey Logvin728b5d02020-11-11 17:16:26 +0000256 if response.status != 200 or resp_json['state'] == 'FAILED':
257 print('Upload failed with %d: %s\n\n%s' % (response.status,
258 response.reason,
259 str(resp_json)))
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100260 return 1
Andrey Logvin728b5d02020-11-11 17:16:26 +0000261
Andrey Logvin25767f72020-11-24 15:48:08 +0000262 print('Upload wasn\'t completed in a given time: %d seconds.',
263 options.wait_timeout_sec)
Andrey Logvin728b5d02020-11-11 17:16:26 +0000264 return 1