blob: de7bd81c73ce009e19ea8a0ff312ecf935a8389c [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
13import subprocess
Andrey Logvin728b5d02020-11-11 17:16:26 +000014import time
Patrik Höglund0569a122020-03-13 12:26:42 +010015import zlib
16
Patrik Höglund620bed12020-03-17 09:59:10 +010017from tracing.value import histogram
Patrik Höglund0569a122020-03-13 12:26:42 +010018from tracing.value import histogram_set
19from tracing.value.diagnostics import generic_set
20from tracing.value.diagnostics import reserved_infos
21
22
23def _GenerateOauthToken():
Mirko Bonadei8cc66952020-10-30 10:13:45 +010024 args = ['luci-auth', 'token']
25 p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
26 if p.wait() == 0:
27 output = p.stdout.read()
28 return output.strip()
29 else:
30 raise RuntimeError(
31 'Error generating authentication token.\nStdout: %s\nStderr:%s' %
32 (p.stdout.read(), p.stderr.read()))
Patrik Höglund0569a122020-03-13 12:26:42 +010033
34
Andrey Logvinbce02a92020-11-24 10:04:50 +000035def _CreateHeaders(oauth_token):
36 return {'Authorization': 'Bearer %s' % oauth_token}
37
38
39def _SendHistogramSet(url, histograms):
Mirko Bonadei8cc66952020-10-30 10:13:45 +010040 """Make a HTTP POST with the given JSON to the Performance Dashboard.
Patrik Höglund0569a122020-03-13 12:26:42 +010041
Andrey Logvin728b5d02020-11-11 17:16:26 +000042 Args:
43 url: URL of Performance Dashboard instance, e.g.
44 "https://chromeperf.appspot.com".
45 histograms: a histogram set object that contains the data to be sent.
Andrey Logvin728b5d02020-11-11 17:16:26 +000046 """
Andrey Logvinbce02a92020-11-24 10:04:50 +000047 headers = _CreateHeaders(_GenerateOauthToken())
Patrik Höglund0569a122020-03-13 12:26:42 +010048
Mirko Bonadei8cc66952020-10-30 10:13:45 +010049 serialized = json.dumps(_ApplyHacks(histograms.AsDicts()), indent=4)
Patrik Höglund0569a122020-03-13 12:26:42 +010050
Mirko Bonadei8cc66952020-10-30 10:13:45 +010051 if url.startswith('http://localhost'):
52 # The catapult server turns off compression in developer mode.
53 data = serialized
54 else:
55 data = zlib.compress(serialized)
Patrik Höglund0569a122020-03-13 12:26:42 +010056
Mirko Bonadei8cc66952020-10-30 10:13:45 +010057 print 'Sending %d bytes to %s.' % (len(data), url + '/add_histograms')
Patrik Höglund0569a122020-03-13 12:26:42 +010058
Mirko Bonadei8cc66952020-10-30 10:13:45 +010059 http = httplib2.Http()
60 response, content = http.request(url + '/add_histograms',
61 method='POST',
62 body=data,
63 headers=headers)
64 return response, content
Patrik Höglund0569a122020-03-13 12:26:42 +010065
66
Andrey Logvinbce02a92020-11-24 10:04:50 +000067def _WaitForUploadConfirmation(url, upload_token, wait_timeout,
Andrey Logvin728b5d02020-11-11 17:16:26 +000068 wait_polling_period):
69 """Make a HTTP GET requests to the Performance Dashboard untill upload
70 status is known or the time is out.
71
72 Args:
73 url: URL of Performance Dashboard instance, e.g.
74 "https://chromeperf.appspot.com".
Andrey Logvin728b5d02020-11-11 17:16:26 +000075 upload_token: String that identifies Performance Dashboard and can be used
76 for the status check.
77 wait_timeout: (datetime.timedelta) Maximum time to wait for the
78 confirmation.
79 wait_polling_period: (datetime.timedelta) Performance Dashboard will be
80 polled every wait_polling_period amount of time.
81 """
82 assert wait_polling_period <= wait_timeout
83
Andrey Logvinbce02a92020-11-24 10:04:50 +000084 headers = _CreateHeaders(_GenerateOauthToken())
Andrey Logvin728b5d02020-11-11 17:16:26 +000085 http = httplib2.Http()
86
Andrey Logvinbce02a92020-11-24 10:04:50 +000087 oauth_refreshed = False
Andrey Logvin728b5d02020-11-11 17:16:26 +000088 response = None
89 resp_json = None
90 current_time = datetime.datetime.now()
91 end_time = current_time + wait_timeout
92 next_poll_time = current_time + wait_polling_period
93 while datetime.datetime.now() < end_time:
94 current_time = datetime.datetime.now()
95 if next_poll_time > current_time:
96 time.sleep((next_poll_time - current_time).total_seconds())
97 next_poll_time = datetime.datetime.now() + wait_polling_period
98
Andrey Logvin9e302ea2020-11-18 16:59:57 +000099 response, content = http.request(url + '/uploads/' + upload_token,
Andrey Logvin728b5d02020-11-11 17:16:26 +0000100 method='GET', headers=headers)
Andrey Logvine850af22020-11-18 15:23:53 +0000101
102 print 'Upload state polled. Response: %r.' % content
103
Andrey Logvinbce02a92020-11-24 10:04:50 +0000104 if not oauth_refreshed and response.status == 403:
105 print 'Oauth token refreshed. Continue polling.'
106 headers = _CreateHeaders(_GenerateOauthToken())
107 oauth_refreshed = True
108 continue
109
Andrey Logvin9e302ea2020-11-18 16:59:57 +0000110 if response.status != 200:
111 break
112
Andrey Logvin728b5d02020-11-11 17:16:26 +0000113 resp_json = json.loads(content)
Andrey Logvin9e302ea2020-11-18 16:59:57 +0000114 if resp_json['state'] == 'COMPLETED' or resp_json['state'] == 'FAILED':
Andrey Logvin728b5d02020-11-11 17:16:26 +0000115 break
116
117 return response, resp_json
118
119
Andrey Logvin844125c2020-11-18 22:07:15 +0000120# Because of an issues on the Dashboard side few measurements over a large set
121# can fail to upload. That would lead to the whole upload to be marked as
122# failed. Check it, so it doesn't increase flakiness of our tests.
123# TODO(crbug.com/1145904): Remove check after fixed.
Andrey Logvinbce02a92020-11-24 10:04:50 +0000124def _CheckFullUploadInfo(url, upload_token,
Andrey Logvin844125c2020-11-18 22:07:15 +0000125 min_measurements_amount=100,
126 max_failed_measurements_amount=1):
127 """Make a HTTP GET requests to the Performance Dashboard to get full info
128 about upload (including measurements). Checks if upload is correct despite
129 not having status "COMPLETED".
130
131 Args:
132 url: URL of Performance Dashboard instance, e.g.
133 "https://chromeperf.appspot.com".
Andrey Logvin844125c2020-11-18 22:07:15 +0000134 upload_token: String that identifies Performance Dashboard and can be used
135 for the status check.
136 min_measurements_amount: minimal amount of measurements that the upload
137 should have to start tolerating failures in particular measurements.
138 max_failed_measurements_amount: maximal amount of failured measurements to
139 tolerate.
140 """
Andrey Logvinbce02a92020-11-24 10:04:50 +0000141 headers = _CreateHeaders(_GenerateOauthToken())
Andrey Logvin844125c2020-11-18 22:07:15 +0000142 http = httplib2.Http()
143
144 response, content = http.request(url + '/uploads/' + upload_token +
145 '?additional_info=measurements',
146 method='GET', headers=headers)
147
148 print 'Full upload info: %r.' % content
149
150 if response.status != 200:
151 print 'Failed to reach the dashboard to get full upload info.'
152 return False
153
154 resp_json = json.loads(content)
155 if 'measurements' in resp_json:
156 measurements_cnt = len(resp_json['measurements'])
157 not_completed_state_cnt = len([
158 m for m in resp_json['measurements']
159 if m['state'] != 'COMPLETED'
160 ])
161
162 if (measurements_cnt >= min_measurements_amount and
163 not_completed_state_cnt <= max_failed_measurements_amount):
164 print('Not all measurements were uploaded. Measurements count: %d, '
165 'failed to upload: %d' %
166 (measurements_cnt, not_completed_state_cnt))
167 return True
168
169 return False
170
171
Patrik Höglund457c8cf2020-03-13 14:43:21 +0100172# TODO(https://crbug.com/1029452): HACKHACK
Andrey Logvinb6b678d2020-11-25 10:33:58 +0000173# Remove once we have doubles in the proto and handle -infinity correctly.
Patrik Höglunda89ad612020-03-13 16:08:08 +0100174def _ApplyHacks(dicts):
Andrey Logvin659d7012020-11-24 15:12:25 +0000175 def _NoInf(value):
176 if value == float('inf'):
177 return histogram.JS_MAX_VALUE
178 if value == float('-inf'):
179 return -histogram.JS_MAX_VALUE
180 return value
181
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100182 for d in dicts:
183 if 'running' in d:
Andrey Logvinb6b678d2020-11-25 10:33:58 +0000184 d['running'] = [_NoInf(value) for value in d['running']]
Andrey Logvin659d7012020-11-24 15:12:25 +0000185 if 'sampleValues' in d:
186 d['sampleValues'] = [_NoInf(value) for value in d['sampleValues']]
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100187
188 return dicts
Patrik Höglund457c8cf2020-03-13 14:43:21 +0100189
190
Patrik Höglund0569a122020-03-13 12:26:42 +0100191def _LoadHistogramSetFromProto(options):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100192 hs = histogram_set.HistogramSet()
193 with options.input_results_file as f:
194 hs.ImportProto(f.read())
Patrik Höglund0569a122020-03-13 12:26:42 +0100195
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100196 return hs
Patrik Höglund0569a122020-03-13 12:26:42 +0100197
198
199def _AddBuildInfo(histograms, options):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100200 common_diagnostics = {
201 reserved_infos.MASTERS: options.perf_dashboard_machine_group,
202 reserved_infos.BOTS: options.bot,
203 reserved_infos.POINT_ID: options.commit_position,
204 reserved_infos.BENCHMARKS: options.test_suite,
205 reserved_infos.WEBRTC_REVISIONS: str(options.webrtc_git_hash),
206 reserved_infos.BUILD_URLS: options.build_page_url,
207 }
Patrik Höglund0569a122020-03-13 12:26:42 +0100208
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100209 for k, v in common_diagnostics.items():
210 histograms.AddSharedDiagnosticToAllHistograms(
211 k.name, generic_set.GenericSet([v]))
Patrik Höglund0569a122020-03-13 12:26:42 +0100212
213
214def _DumpOutput(histograms, output_file):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100215 with output_file:
216 json.dump(_ApplyHacks(histograms.AsDicts()), output_file, indent=4)
Patrik Höglund0569a122020-03-13 12:26:42 +0100217
218
Patrik Höglund0569a122020-03-13 12:26:42 +0100219def UploadToDashboard(options):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100220 histograms = _LoadHistogramSetFromProto(options)
221 _AddBuildInfo(histograms, options)
Patrik Höglund0569a122020-03-13 12:26:42 +0100222
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100223 if options.output_json_file:
224 _DumpOutput(histograms, options.output_json_file)
Patrik Höglund0569a122020-03-13 12:26:42 +0100225
Andrey Logvinbce02a92020-11-24 10:04:50 +0000226 response, content = _SendHistogramSet(options.dashboard_url, histograms)
Patrik Höglund0569a122020-03-13 12:26:42 +0100227
Andrey Logvinbcca3b02020-11-27 15:06:48 +0000228 if response.status != 200:
229 print('Upload failed with %d: %s\n\n%s' % (response.status,
230 response.reason, content))
231 return 1
232
Andrey Logvin728b5d02020-11-11 17:16:26 +0000233 upload_token = json.loads(content).get('token')
234 if not options.wait_for_upload or not upload_token:
Andrey Logvinbcca3b02020-11-27 15:06:48 +0000235 print('Received 200 from dashboard. ',
236 'Not waiting for the upload status confirmation.')
237 return 0
Andrey Logvin728b5d02020-11-11 17:16:26 +0000238
239 response, resp_json = _WaitForUploadConfirmation(
240 options.dashboard_url,
Andrey Logvin728b5d02020-11-11 17:16:26 +0000241 upload_token,
242 datetime.timedelta(seconds=options.wait_timeout_sec),
243 datetime.timedelta(seconds=options.wait_polling_period_sec))
244
Andrey Logvin844125c2020-11-18 22:07:15 +0000245 if ((resp_json and resp_json['state'] == 'COMPLETED') or
Andrey Logvinbce02a92020-11-24 10:04:50 +0000246 _CheckFullUploadInfo(options.dashboard_url, upload_token)):
Andrey Logvin844125c2020-11-18 22:07:15 +0000247 print 'Upload completed.'
248 return 0
249
Andrey Logvin728b5d02020-11-11 17:16:26 +0000250 if response.status != 200 or resp_json['state'] == 'FAILED':
251 print('Upload failed with %d: %s\n\n%s' % (response.status,
252 response.reason,
253 str(resp_json)))
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100254 return 1
Andrey Logvin728b5d02020-11-11 17:16:26 +0000255
Andrey Logvinbcca3b02020-11-27 15:06:48 +0000256 print('Upload wasn\'t completed in a given time: %d seconds.' %
Andrey Logvin25767f72020-11-24 15:48:08 +0000257 options.wait_timeout_sec)
Andrey Logvin728b5d02020-11-11 17:16:26 +0000258 return 1