blob: d07c287f28285d685a6ca8ae3690042b96e3d1fc [file] [log] [blame]
Christoffer Jansson4e8a7732022-02-08 09:01:12 +01001#!/usr/bin/env vpython3
2
Patrik Höglund0569a122020-03-13 12:26:42 +01003# Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
4#
5# Use of this source code is governed by a BSD-style license
6# that can be found in the LICENSE file in the root of the source
7# tree. An additional intellectual property rights grant can be found
8# in the file PATENTS. All contributing project authors may
9# be found in the AUTHORS file in the root of the source tree.
10
Andrey Logvin728b5d02020-11-11 17:16:26 +000011import datetime
Patrik Höglund0569a122020-03-13 12:26:42 +010012import json
13import subprocess
Andrey Logvin728b5d02020-11-11 17:16:26 +000014import time
Patrik Höglund0569a122020-03-13 12:26:42 +010015import zlib
16
Jeremy Leconte4fc9bd92022-03-18 10:21:07 +010017from typing import Optional
Jeremy Leconte2c4a4472022-03-14 15:22:37 +010018import dataclasses
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010019import httplib2
20
Patrik Höglund620bed12020-03-17 09:59:10 +010021from tracing.value import histogram
Patrik Höglund0569a122020-03-13 12:26:42 +010022from tracing.value import histogram_set
23from tracing.value.diagnostics import generic_set
24from tracing.value.diagnostics import reserved_infos
25
26
Jeremy Leconte2c4a4472022-03-14 15:22:37 +010027@dataclasses.dataclass
28class UploaderOptions():
29 """Required information to upload perf metrics.
30
31 Attributes:
32 perf_dashboard_machine_group: The "master" the bots are grouped under.
33 This string is the group in the the perf dashboard path
34 group/bot/perf_id/metric/subtest.
35 bot: The bot running the test (e.g. webrtc-win-large-tests).
36 test_suite: The key for the test in the dashboard (i.e. what you select
37 in the top-level test suite selector in the dashboard
38 webrtc_git_hash: webrtc.googlesource.com commit hash.
39 commit_position: Commit pos corresponding to the git hash.
40 build_page_url: URL to the build page for this build.
41 dashboard_url: Which dashboard to use.
42 input_results_file: A HistogramSet proto file coming from WebRTC tests.
43 output_json_file: Where to write the output (for debugging).
44 wait_timeout_sec: Maximum amount of time in seconds that the script will
45 wait for the confirmation.
46 wait_polling_period_sec: Status will be requested from the Dashboard
47 every wait_polling_period_sec seconds.
48 """
49 perf_dashboard_machine_group: str
50 bot: str
51 test_suite: str
52 webrtc_git_hash: str
53 commit_position: int
54 build_page_url: str
55 dashboard_url: str
56 input_results_file: str
Jeremy Leconte4fc9bd92022-03-18 10:21:07 +010057 output_json_file: Optional[str] = None
Jeremy Leconte2c4a4472022-03-14 15:22:37 +010058 wait_timeout_sec: datetime.timedelta = datetime.timedelta(seconds=1200)
59 wait_polling_period_sec: datetime.timedelta = datetime.timedelta(seconds=120)
60
61
Patrik Höglund0569a122020-03-13 12:26:42 +010062def _GenerateOauthToken():
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010063 args = ['luci-auth', 'token']
Christoffer Jansson409ac892022-02-08 18:24:29 +010064 p = subprocess.Popen(args,
65 universal_newlines=True,
66 stdout=subprocess.PIPE,
67 stderr=subprocess.PIPE)
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010068 if p.wait() == 0:
69 output = p.stdout.read()
70 return output.strip()
71 raise RuntimeError(
72 'Error generating authentication token.\nStdout: %s\nStderr:%s' %
73 (p.stdout.read(), p.stderr.read()))
Patrik Höglund0569a122020-03-13 12:26:42 +010074
75
Andrey Logvinbce02a92020-11-24 10:04:50 +000076def _CreateHeaders(oauth_token):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010077 return {'Authorization': 'Bearer %s' % oauth_token}
Andrey Logvinbce02a92020-11-24 10:04:50 +000078
79
80def _SendHistogramSet(url, histograms):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010081 """Make a HTTP POST with the given JSON to the Performance Dashboard.
Patrik Höglund0569a122020-03-13 12:26:42 +010082
Andrey Logvin728b5d02020-11-11 17:16:26 +000083 Args:
84 url: URL of Performance Dashboard instance, e.g.
85 "https://chromeperf.appspot.com".
86 histograms: a histogram set object that contains the data to be sent.
Andrey Logvin728b5d02020-11-11 17:16:26 +000087 """
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010088 headers = _CreateHeaders(_GenerateOauthToken())
Patrik Höglund0569a122020-03-13 12:26:42 +010089
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010090 serialized = json.dumps(_ApplyHacks(histograms.AsDicts()), indent=4)
Patrik Höglund0569a122020-03-13 12:26:42 +010091
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010092 if url.startswith('http://localhost'):
93 # The catapult server turns off compression in developer mode.
94 data = serialized
95 else:
Christoffer Jansson1b083a92022-02-15 14:52:31 +010096 data = zlib.compress(serialized.encode('utf-8'))
Patrik Höglund0569a122020-03-13 12:26:42 +010097
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010098 print('Sending %d bytes to %s.' % (len(data), url + '/add_histograms'))
Patrik Höglund0569a122020-03-13 12:26:42 +010099
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100100 http = httplib2.Http()
101 response, content = http.request(url + '/add_histograms',
102 method='POST',
103 body=data,
104 headers=headers)
105 return response, content
Patrik Höglund0569a122020-03-13 12:26:42 +0100106
107
Andrey Logvinbce02a92020-11-24 10:04:50 +0000108def _WaitForUploadConfirmation(url, upload_token, wait_timeout,
Andrey Logvin728b5d02020-11-11 17:16:26 +0000109 wait_polling_period):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100110 """Make a HTTP GET requests to the Performance Dashboard untill upload
Andrey Logvin728b5d02020-11-11 17:16:26 +0000111 status is known or the time is out.
112
113 Args:
114 url: URL of Performance Dashboard instance, e.g.
115 "https://chromeperf.appspot.com".
Andrey Logvin728b5d02020-11-11 17:16:26 +0000116 upload_token: String that identifies Performance Dashboard and can be used
117 for the status check.
118 wait_timeout: (datetime.timedelta) Maximum time to wait for the
119 confirmation.
120 wait_polling_period: (datetime.timedelta) Performance Dashboard will be
121 polled every wait_polling_period amount of time.
122 """
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100123 assert wait_polling_period <= wait_timeout
Andrey Logvin728b5d02020-11-11 17:16:26 +0000124
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100125 headers = _CreateHeaders(_GenerateOauthToken())
126 http = httplib2.Http()
Andrey Logvin728b5d02020-11-11 17:16:26 +0000127
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100128 oauth_refreshed = False
129 response = None
130 resp_json = None
131 current_time = datetime.datetime.now()
132 end_time = current_time + wait_timeout
133 next_poll_time = current_time + wait_polling_period
134 while datetime.datetime.now() < end_time:
Andrey Logvin728b5d02020-11-11 17:16:26 +0000135 current_time = datetime.datetime.now()
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100136 if next_poll_time > current_time:
137 time.sleep((next_poll_time - current_time).total_seconds())
138 next_poll_time = datetime.datetime.now() + wait_polling_period
Andrey Logvin728b5d02020-11-11 17:16:26 +0000139
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100140 response, content = http.request(url + '/uploads/' + upload_token,
141 method='GET',
142 headers=headers)
Andrey Logvine850af22020-11-18 15:23:53 +0000143
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100144 print('Upload state polled. Response: %r.' % content)
Andrey Logvine850af22020-11-18 15:23:53 +0000145
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100146 if not oauth_refreshed and response.status == 403:
147 print('Oauth token refreshed. Continue polling.')
148 headers = _CreateHeaders(_GenerateOauthToken())
149 oauth_refreshed = True
150 continue
Andrey Logvinbce02a92020-11-24 10:04:50 +0000151
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100152 if response.status != 200:
153 break
Andrey Logvin9e302ea2020-11-18 16:59:57 +0000154
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100155 resp_json = json.loads(content)
156 if resp_json['state'] == 'COMPLETED' or resp_json['state'] == 'FAILED':
157 break
Andrey Logvin728b5d02020-11-11 17:16:26 +0000158
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100159 return response, resp_json
Andrey Logvin728b5d02020-11-11 17:16:26 +0000160
161
Andrey Logvin844125c2020-11-18 22:07:15 +0000162# Because of an issues on the Dashboard side few measurements over a large set
163# can fail to upload. That would lead to the whole upload to be marked as
164# failed. Check it, so it doesn't increase flakiness of our tests.
165# TODO(crbug.com/1145904): Remove check after fixed.
Andrey Logvinbce02a92020-11-24 10:04:50 +0000166def _CheckFullUploadInfo(url, upload_token,
landrey722a8a62021-08-12 16:27:56 +0000167 min_measurements_amount=50,
168 max_failed_measurements_percent=0.03):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100169 """Make a HTTP GET requests to the Performance Dashboard to get full info
Andrey Logvin844125c2020-11-18 22:07:15 +0000170 about upload (including measurements). Checks if upload is correct despite
171 not having status "COMPLETED".
172
173 Args:
174 url: URL of Performance Dashboard instance, e.g.
175 "https://chromeperf.appspot.com".
Andrey Logvin844125c2020-11-18 22:07:15 +0000176 upload_token: String that identifies Performance Dashboard and can be used
177 for the status check.
178 min_measurements_amount: minimal amount of measurements that the upload
179 should have to start tolerating failures in particular measurements.
landrey722a8a62021-08-12 16:27:56 +0000180 max_failed_measurements_percent: maximal percent of failured measurements
181 to tolerate.
Andrey Logvin844125c2020-11-18 22:07:15 +0000182 """
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100183 headers = _CreateHeaders(_GenerateOauthToken())
184 http = httplib2.Http()
Andrey Logvin844125c2020-11-18 22:07:15 +0000185
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100186 response, content = http.request(url + '/uploads/' + upload_token +
187 '?additional_info=measurements',
188 method='GET',
189 headers=headers)
Andrey Logvin844125c2020-11-18 22:07:15 +0000190
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100191 if response.status != 200:
192 print('Failed to reach the dashboard to get full upload info.')
Andrey Logvin844125c2020-11-18 22:07:15 +0000193 return False
194
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100195 resp_json = json.loads(content)
196 print('Full upload info: %s.' % json.dumps(resp_json, indent=4))
197
198 if 'measurements' in resp_json:
199 measurements_cnt = len(resp_json['measurements'])
200 not_completed_state_cnt = len(
201 [m for m in resp_json['measurements'] if m['state'] != 'COMPLETED'])
202
203 if (measurements_cnt >= min_measurements_amount
204 and (not_completed_state_cnt /
205 (measurements_cnt * 1.0) <= max_failed_measurements_percent)):
206 print(('Not all measurements were confirmed to upload. '
207 'Measurements count: %d, failed to upload or timed out: %d' %
208 (measurements_cnt, not_completed_state_cnt)))
209 return True
210
211 return False
212
Andrey Logvin844125c2020-11-18 22:07:15 +0000213
Patrik Höglund457c8cf2020-03-13 14:43:21 +0100214# TODO(https://crbug.com/1029452): HACKHACK
Andrey Logvinb6b678d2020-11-25 10:33:58 +0000215# Remove once we have doubles in the proto and handle -infinity correctly.
Patrik Höglunda89ad612020-03-13 16:08:08 +0100216def _ApplyHacks(dicts):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100217 def _NoInf(value):
218 if value == float('inf'):
219 return histogram.JS_MAX_VALUE
220 if value == float('-inf'):
221 return -histogram.JS_MAX_VALUE
222 return value
Andrey Logvin659d7012020-11-24 15:12:25 +0000223
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100224 for d in dicts:
225 if 'running' in d:
226 d['running'] = [_NoInf(value) for value in d['running']]
227 if 'sampleValues' in d:
228 d['sampleValues'] = [_NoInf(value) for value in d['sampleValues']]
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100229
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100230 return dicts
Patrik Höglund457c8cf2020-03-13 14:43:21 +0100231
232
Patrik Höglund0569a122020-03-13 12:26:42 +0100233def _LoadHistogramSetFromProto(options):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100234 hs = histogram_set.HistogramSet()
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100235 with open(options.input_results_file, 'rb') as f:
Christoffer Janssonc98fb2c2022-02-08 21:43:45 +0100236 hs.ImportProto(f.read())
Patrik Höglund0569a122020-03-13 12:26:42 +0100237
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100238 return hs
Patrik Höglund0569a122020-03-13 12:26:42 +0100239
240
241def _AddBuildInfo(histograms, options):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100242 common_diagnostics = {
243 reserved_infos.MASTERS: options.perf_dashboard_machine_group,
244 reserved_infos.BOTS: options.bot,
245 reserved_infos.POINT_ID: options.commit_position,
246 reserved_infos.BENCHMARKS: options.test_suite,
247 reserved_infos.WEBRTC_REVISIONS: str(options.webrtc_git_hash),
248 reserved_infos.BUILD_URLS: options.build_page_url,
249 }
Patrik Höglund0569a122020-03-13 12:26:42 +0100250
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100251 for k, v in list(common_diagnostics.items()):
252 histograms.AddSharedDiagnosticToAllHistograms(k.name,
253 generic_set.GenericSet([v]))
Patrik Höglund0569a122020-03-13 12:26:42 +0100254
255
256def _DumpOutput(histograms, output_file):
Jeremy Lecontefa577c52022-03-14 20:06:11 +0100257 with open(output_file, 'w') as f:
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100258 json.dump(_ApplyHacks(histograms.AsDicts()), f, indent=4)
Patrik Höglund0569a122020-03-13 12:26:42 +0100259
260
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100261def UploadToDashboardImpl(options):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100262 histograms = _LoadHistogramSetFromProto(options)
263 _AddBuildInfo(histograms, options)
Patrik Höglund0569a122020-03-13 12:26:42 +0100264
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100265 if options.output_json_file:
266 _DumpOutput(histograms, options.output_json_file)
Patrik Höglund0569a122020-03-13 12:26:42 +0100267
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100268 response, content = _SendHistogramSet(options.dashboard_url, histograms)
Patrik Höglund0569a122020-03-13 12:26:42 +0100269
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100270 if response.status != 200:
271 print(('Upload failed with %d: %s\n\n%s' %
272 (response.status, response.reason, content)))
Andrey Logvin728b5d02020-11-11 17:16:26 +0000273 return 1
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100274
275 upload_token = json.loads(content).get('token')
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100276 if not upload_token:
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100277 print(('Received 200 from dashboard. ',
278 'Not waiting for the upload status confirmation.'))
279 return 0
280
281 response, resp_json = _WaitForUploadConfirmation(
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100282 options.dashboard_url, upload_token, options.wait_timeout_sec,
283 options.wait_polling_period_sec)
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100284
285 if ((resp_json and resp_json['state'] == 'COMPLETED')
286 or _CheckFullUploadInfo(options.dashboard_url, upload_token)):
287 print('Upload completed.')
288 return 0
289
290 if response.status != 200:
291 print(('Upload status poll failed with %d: %s' %
292 (response.status, response.reason)))
293 return 1
294
295 if resp_json['state'] == 'FAILED':
296 print('Upload failed.')
297 return 1
298
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100299 print(('Upload wasn\'t completed in a given time: %s seconds.' %
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100300 options.wait_timeout_sec))
301 return 1
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100302
303
304def UploadToDashboard(options):
305 try:
306 exit_code = UploadToDashboardImpl(options)
307 except RuntimeError as e:
308 print(e)
Jeremy Leconte4fc9bd92022-03-18 10:21:07 +0100309 return 1
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100310 return exit_code