blob: f1e564cacd1faa084083b860e2f12b96846b41ae [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
35def _SendHistogramSet(url, histograms, oauth_token):
Mirko Bonadei8cc66952020-10-30 10:13:45 +010036 """Make a HTTP POST with the given JSON to the Performance Dashboard.
Patrik Höglund0569a122020-03-13 12:26:42 +010037
Andrey Logvin728b5d02020-11-11 17:16:26 +000038 Args:
39 url: URL of Performance Dashboard instance, e.g.
40 "https://chromeperf.appspot.com".
41 histograms: a histogram set object that contains the data to be sent.
42 oauth_token: An oauth token to use for authorization.
43 """
Mirko Bonadei8cc66952020-10-30 10:13:45 +010044 headers = {'Authorization': 'Bearer %s' % oauth_token}
Patrik Höglund0569a122020-03-13 12:26:42 +010045
Mirko Bonadei8cc66952020-10-30 10:13:45 +010046 serialized = json.dumps(_ApplyHacks(histograms.AsDicts()), indent=4)
Patrik Höglund0569a122020-03-13 12:26:42 +010047
Mirko Bonadei8cc66952020-10-30 10:13:45 +010048 if url.startswith('http://localhost'):
49 # The catapult server turns off compression in developer mode.
50 data = serialized
51 else:
52 data = zlib.compress(serialized)
Patrik Höglund0569a122020-03-13 12:26:42 +010053
Mirko Bonadei8cc66952020-10-30 10:13:45 +010054 print 'Sending %d bytes to %s.' % (len(data), url + '/add_histograms')
Patrik Höglund0569a122020-03-13 12:26:42 +010055
Mirko Bonadei8cc66952020-10-30 10:13:45 +010056 http = httplib2.Http()
57 response, content = http.request(url + '/add_histograms',
58 method='POST',
59 body=data,
60 headers=headers)
61 return response, content
Patrik Höglund0569a122020-03-13 12:26:42 +010062
63
Andrey Logvin728b5d02020-11-11 17:16:26 +000064def _WaitForUploadConfirmation(url, oauth_token, upload_token, wait_timeout,
65 wait_polling_period):
66 """Make a HTTP GET requests to the Performance Dashboard untill upload
67 status is known or the time is out.
68
69 Args:
70 url: URL of Performance Dashboard instance, e.g.
71 "https://chromeperf.appspot.com".
72 oauth_token: An oauth token to use for authorization.
73 upload_token: String that identifies Performance Dashboard and can be used
74 for the status check.
75 wait_timeout: (datetime.timedelta) Maximum time to wait for the
76 confirmation.
77 wait_polling_period: (datetime.timedelta) Performance Dashboard will be
78 polled every wait_polling_period amount of time.
79 """
80 assert wait_polling_period <= wait_timeout
81
82 headers = {'Authorization': 'Bearer %s' % oauth_token}
83 http = httplib2.Http()
84
85 response = None
86 resp_json = None
87 current_time = datetime.datetime.now()
88 end_time = current_time + wait_timeout
89 next_poll_time = current_time + wait_polling_period
90 while datetime.datetime.now() < end_time:
91 current_time = datetime.datetime.now()
92 if next_poll_time > current_time:
93 time.sleep((next_poll_time - current_time).total_seconds())
94 next_poll_time = datetime.datetime.now() + wait_polling_period
95
Andrey Logvin9e302ea2020-11-18 16:59:57 +000096 response, content = http.request(url + '/uploads/' + upload_token,
Andrey Logvin728b5d02020-11-11 17:16:26 +000097 method='GET', headers=headers)
Andrey Logvine850af22020-11-18 15:23:53 +000098
99 print 'Upload state polled. Response: %r.' % content
100
Andrey Logvin9e302ea2020-11-18 16:59:57 +0000101 if response.status != 200:
102 break
103
Andrey Logvin728b5d02020-11-11 17:16:26 +0000104 resp_json = json.loads(content)
Andrey Logvin9e302ea2020-11-18 16:59:57 +0000105 if resp_json['state'] == 'COMPLETED' or resp_json['state'] == 'FAILED':
Andrey Logvin728b5d02020-11-11 17:16:26 +0000106 break
107
108 return response, resp_json
109
110
Andrey Logvin844125c2020-11-18 22:07:15 +0000111# Because of an issues on the Dashboard side few measurements over a large set
112# can fail to upload. That would lead to the whole upload to be marked as
113# failed. Check it, so it doesn't increase flakiness of our tests.
114# TODO(crbug.com/1145904): Remove check after fixed.
115def _CheckFullUploadInfo(url, oauth_token, upload_token,
116 min_measurements_amount=100,
117 max_failed_measurements_amount=1):
118 """Make a HTTP GET requests to the Performance Dashboard to get full info
119 about upload (including measurements). Checks if upload is correct despite
120 not having status "COMPLETED".
121
122 Args:
123 url: URL of Performance Dashboard instance, e.g.
124 "https://chromeperf.appspot.com".
125 oauth_token: An oauth token to use for authorization.
126 upload_token: String that identifies Performance Dashboard and can be used
127 for the status check.
128 min_measurements_amount: minimal amount of measurements that the upload
129 should have to start tolerating failures in particular measurements.
130 max_failed_measurements_amount: maximal amount of failured measurements to
131 tolerate.
132 """
133 headers = {'Authorization': 'Bearer %s' % oauth_token}
134 http = httplib2.Http()
135
136 response, content = http.request(url + '/uploads/' + upload_token +
137 '?additional_info=measurements',
138 method='GET', headers=headers)
139
140 print 'Full upload info: %r.' % content
141
142 if response.status != 200:
143 print 'Failed to reach the dashboard to get full upload info.'
144 return False
145
146 resp_json = json.loads(content)
147 if 'measurements' in resp_json:
148 measurements_cnt = len(resp_json['measurements'])
149 not_completed_state_cnt = len([
150 m for m in resp_json['measurements']
151 if m['state'] != 'COMPLETED'
152 ])
153
154 if (measurements_cnt >= min_measurements_amount and
155 not_completed_state_cnt <= max_failed_measurements_amount):
156 print('Not all measurements were uploaded. Measurements count: %d, '
157 'failed to upload: %d' %
158 (measurements_cnt, not_completed_state_cnt))
159 return True
160
161 return False
162
163
Patrik Höglund457c8cf2020-03-13 14:43:21 +0100164# TODO(https://crbug.com/1029452): HACKHACK
Patrik Höglund620bed12020-03-17 09:59:10 +0100165# Remove once we have doubles in the proto and handle -infinity correctly.
Patrik Höglunda89ad612020-03-13 16:08:08 +0100166def _ApplyHacks(dicts):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100167 for d in dicts:
168 if 'running' in d:
Patrik Höglund457c8cf2020-03-13 14:43:21 +0100169
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100170 def _NoInf(value):
171 if value == float('inf'):
172 return histogram.JS_MAX_VALUE
173 if value == float('-inf'):
174 return -histogram.JS_MAX_VALUE
175 return value
176
177 d['running'] = [_NoInf(value) for value in d['running']]
178
179 return dicts
Patrik Höglund457c8cf2020-03-13 14:43:21 +0100180
181
Patrik Höglund0569a122020-03-13 12:26:42 +0100182def _LoadHistogramSetFromProto(options):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100183 hs = histogram_set.HistogramSet()
184 with options.input_results_file as f:
185 hs.ImportProto(f.read())
Patrik Höglund0569a122020-03-13 12:26:42 +0100186
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100187 return hs
Patrik Höglund0569a122020-03-13 12:26:42 +0100188
189
190def _AddBuildInfo(histograms, options):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100191 common_diagnostics = {
192 reserved_infos.MASTERS: options.perf_dashboard_machine_group,
193 reserved_infos.BOTS: options.bot,
194 reserved_infos.POINT_ID: options.commit_position,
195 reserved_infos.BENCHMARKS: options.test_suite,
196 reserved_infos.WEBRTC_REVISIONS: str(options.webrtc_git_hash),
197 reserved_infos.BUILD_URLS: options.build_page_url,
198 }
Patrik Höglund0569a122020-03-13 12:26:42 +0100199
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100200 for k, v in common_diagnostics.items():
201 histograms.AddSharedDiagnosticToAllHistograms(
202 k.name, generic_set.GenericSet([v]))
Patrik Höglund0569a122020-03-13 12:26:42 +0100203
204
205def _DumpOutput(histograms, output_file):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100206 with output_file:
207 json.dump(_ApplyHacks(histograms.AsDicts()), output_file, indent=4)
Patrik Höglund0569a122020-03-13 12:26:42 +0100208
209
Patrik Höglund0569a122020-03-13 12:26:42 +0100210def UploadToDashboard(options):
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100211 histograms = _LoadHistogramSetFromProto(options)
212 _AddBuildInfo(histograms, options)
Patrik Höglund0569a122020-03-13 12:26:42 +0100213
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100214 if options.output_json_file:
215 _DumpOutput(histograms, options.output_json_file)
Patrik Höglund0569a122020-03-13 12:26:42 +0100216
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100217 oauth_token = _GenerateOauthToken()
Andrey Logvin728b5d02020-11-11 17:16:26 +0000218 response, content = _SendHistogramSet(
219 options.dashboard_url, histograms, oauth_token)
Patrik Höglund0569a122020-03-13 12:26:42 +0100220
Andrey Logvin728b5d02020-11-11 17:16:26 +0000221 upload_token = json.loads(content).get('token')
222 if not options.wait_for_upload or not upload_token:
223 print 'Not waiting for upload status confirmation.'
224 if response.status == 200:
225 print 'Received 200 from dashboard.'
226 return 0
227 else:
228 print('Upload failed with %d: %s\n\n%s' % (response.status,
229 response.reason, content))
230 return 1
231
232 response, resp_json = _WaitForUploadConfirmation(
233 options.dashboard_url,
234 oauth_token,
235 upload_token,
236 datetime.timedelta(seconds=options.wait_timeout_sec),
237 datetime.timedelta(seconds=options.wait_polling_period_sec))
238
Andrey Logvin844125c2020-11-18 22:07:15 +0000239 if ((resp_json and resp_json['state'] == 'COMPLETED') or
240 _CheckFullUploadInfo(options.dashboard_url, oauth_token, upload_token)):
241 print 'Upload completed.'
242 return 0
243
Andrey Logvin728b5d02020-11-11 17:16:26 +0000244 if response.status != 200 or resp_json['state'] == 'FAILED':
245 print('Upload failed with %d: %s\n\n%s' % (response.status,
246 response.reason,
247 str(resp_json)))
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100248 return 1
Andrey Logvin728b5d02020-11-11 17:16:26 +0000249
Andrey Logvin728b5d02020-11-11 17:16:26 +0000250 print('Upload wasn\'t completed in a given time: %d.', options.wait_timeout)
251 return 1