blob: c33ac89d0cf74500e059cd519f4f9f76f02128b3 [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 Leconte2c4a4472022-03-14 15:22:37 +010017import dataclasses
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010018import httplib2
19
Patrik Höglund620bed12020-03-17 09:59:10 +010020from tracing.value import histogram
Patrik Höglund0569a122020-03-13 12:26:42 +010021from tracing.value import histogram_set
22from tracing.value.diagnostics import generic_set
23from tracing.value.diagnostics import reserved_infos
24
25
Jeremy Leconte2c4a4472022-03-14 15:22:37 +010026@dataclasses.dataclass
27class UploaderOptions():
28 """Required information to upload perf metrics.
29
30 Attributes:
31 perf_dashboard_machine_group: The "master" the bots are grouped under.
32 This string is the group in the the perf dashboard path
33 group/bot/perf_id/metric/subtest.
34 bot: The bot running the test (e.g. webrtc-win-large-tests).
35 test_suite: The key for the test in the dashboard (i.e. what you select
36 in the top-level test suite selector in the dashboard
37 webrtc_git_hash: webrtc.googlesource.com commit hash.
38 commit_position: Commit pos corresponding to the git hash.
39 build_page_url: URL to the build page for this build.
40 dashboard_url: Which dashboard to use.
41 input_results_file: A HistogramSet proto file coming from WebRTC tests.
42 output_json_file: Where to write the output (for debugging).
43 wait_timeout_sec: Maximum amount of time in seconds that the script will
44 wait for the confirmation.
45 wait_polling_period_sec: Status will be requested from the Dashboard
46 every wait_polling_period_sec seconds.
47 """
48 perf_dashboard_machine_group: str
49 bot: str
50 test_suite: str
51 webrtc_git_hash: str
52 commit_position: int
53 build_page_url: str
54 dashboard_url: str
55 input_results_file: str
56 output_json_file: str
57 wait_timeout_sec: datetime.timedelta = datetime.timedelta(seconds=1200)
58 wait_polling_period_sec: datetime.timedelta = datetime.timedelta(seconds=120)
59
60
Patrik Höglund0569a122020-03-13 12:26:42 +010061def _GenerateOauthToken():
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010062 args = ['luci-auth', 'token']
Christoffer Jansson409ac892022-02-08 18:24:29 +010063 p = subprocess.Popen(args,
64 universal_newlines=True,
65 stdout=subprocess.PIPE,
66 stderr=subprocess.PIPE)
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010067 if p.wait() == 0:
68 output = p.stdout.read()
69 return output.strip()
70 raise RuntimeError(
71 'Error generating authentication token.\nStdout: %s\nStderr:%s' %
72 (p.stdout.read(), p.stderr.read()))
Patrik Höglund0569a122020-03-13 12:26:42 +010073
74
Andrey Logvinbce02a92020-11-24 10:04:50 +000075def _CreateHeaders(oauth_token):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010076 return {'Authorization': 'Bearer %s' % oauth_token}
Andrey Logvinbce02a92020-11-24 10:04:50 +000077
78
79def _SendHistogramSet(url, histograms):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010080 """Make a HTTP POST with the given JSON to the Performance Dashboard.
Patrik Höglund0569a122020-03-13 12:26:42 +010081
Andrey Logvin728b5d02020-11-11 17:16:26 +000082 Args:
83 url: URL of Performance Dashboard instance, e.g.
84 "https://chromeperf.appspot.com".
85 histograms: a histogram set object that contains the data to be sent.
Andrey Logvin728b5d02020-11-11 17:16:26 +000086 """
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010087 headers = _CreateHeaders(_GenerateOauthToken())
Patrik Höglund0569a122020-03-13 12:26:42 +010088
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010089 serialized = json.dumps(_ApplyHacks(histograms.AsDicts()), indent=4)
Patrik Höglund0569a122020-03-13 12:26:42 +010090
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010091 if url.startswith('http://localhost'):
92 # The catapult server turns off compression in developer mode.
93 data = serialized
94 else:
Christoffer Jansson1b083a92022-02-15 14:52:31 +010095 data = zlib.compress(serialized.encode('utf-8'))
Patrik Höglund0569a122020-03-13 12:26:42 +010096
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010097 print('Sending %d bytes to %s.' % (len(data), url + '/add_histograms'))
Patrik Höglund0569a122020-03-13 12:26:42 +010098
Christoffer Jansson4e8a7732022-02-08 09:01:12 +010099 http = httplib2.Http()
100 response, content = http.request(url + '/add_histograms',
101 method='POST',
102 body=data,
103 headers=headers)
104 return response, content
Patrik Höglund0569a122020-03-13 12:26:42 +0100105
106
Andrey Logvinbce02a92020-11-24 10:04:50 +0000107def _WaitForUploadConfirmation(url, upload_token, wait_timeout,
Andrey Logvin728b5d02020-11-11 17:16:26 +0000108 wait_polling_period):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100109 """Make a HTTP GET requests to the Performance Dashboard untill upload
Andrey Logvin728b5d02020-11-11 17:16:26 +0000110 status is known or the time is out.
111
112 Args:
113 url: URL of Performance Dashboard instance, e.g.
114 "https://chromeperf.appspot.com".
Andrey Logvin728b5d02020-11-11 17:16:26 +0000115 upload_token: String that identifies Performance Dashboard and can be used
116 for the status check.
117 wait_timeout: (datetime.timedelta) Maximum time to wait for the
118 confirmation.
119 wait_polling_period: (datetime.timedelta) Performance Dashboard will be
120 polled every wait_polling_period amount of time.
121 """
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100122 assert wait_polling_period <= wait_timeout
Andrey Logvin728b5d02020-11-11 17:16:26 +0000123
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100124 headers = _CreateHeaders(_GenerateOauthToken())
125 http = httplib2.Http()
Andrey Logvin728b5d02020-11-11 17:16:26 +0000126
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100127 oauth_refreshed = False
128 response = None
129 resp_json = None
130 current_time = datetime.datetime.now()
131 end_time = current_time + wait_timeout
132 next_poll_time = current_time + wait_polling_period
133 while datetime.datetime.now() < end_time:
Andrey Logvin728b5d02020-11-11 17:16:26 +0000134 current_time = datetime.datetime.now()
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100135 if next_poll_time > current_time:
136 time.sleep((next_poll_time - current_time).total_seconds())
137 next_poll_time = datetime.datetime.now() + wait_polling_period
Andrey Logvin728b5d02020-11-11 17:16:26 +0000138
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100139 response, content = http.request(url + '/uploads/' + upload_token,
140 method='GET',
141 headers=headers)
Andrey Logvine850af22020-11-18 15:23:53 +0000142
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100143 print('Upload state polled. Response: %r.' % content)
Andrey Logvine850af22020-11-18 15:23:53 +0000144
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100145 if not oauth_refreshed and response.status == 403:
146 print('Oauth token refreshed. Continue polling.')
147 headers = _CreateHeaders(_GenerateOauthToken())
148 oauth_refreshed = True
149 continue
Andrey Logvinbce02a92020-11-24 10:04:50 +0000150
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100151 if response.status != 200:
152 break
Andrey Logvin9e302ea2020-11-18 16:59:57 +0000153
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100154 resp_json = json.loads(content)
155 if resp_json['state'] == 'COMPLETED' or resp_json['state'] == 'FAILED':
156 break
Andrey Logvin728b5d02020-11-11 17:16:26 +0000157
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100158 return response, resp_json
Andrey Logvin728b5d02020-11-11 17:16:26 +0000159
160
Andrey Logvin844125c2020-11-18 22:07:15 +0000161# Because of an issues on the Dashboard side few measurements over a large set
162# can fail to upload. That would lead to the whole upload to be marked as
163# failed. Check it, so it doesn't increase flakiness of our tests.
164# TODO(crbug.com/1145904): Remove check after fixed.
Andrey Logvinbce02a92020-11-24 10:04:50 +0000165def _CheckFullUploadInfo(url, upload_token,
landrey722a8a62021-08-12 16:27:56 +0000166 min_measurements_amount=50,
167 max_failed_measurements_percent=0.03):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100168 """Make a HTTP GET requests to the Performance Dashboard to get full info
Andrey Logvin844125c2020-11-18 22:07:15 +0000169 about upload (including measurements). Checks if upload is correct despite
170 not having status "COMPLETED".
171
172 Args:
173 url: URL of Performance Dashboard instance, e.g.
174 "https://chromeperf.appspot.com".
Andrey Logvin844125c2020-11-18 22:07:15 +0000175 upload_token: String that identifies Performance Dashboard and can be used
176 for the status check.
177 min_measurements_amount: minimal amount of measurements that the upload
178 should have to start tolerating failures in particular measurements.
landrey722a8a62021-08-12 16:27:56 +0000179 max_failed_measurements_percent: maximal percent of failured measurements
180 to tolerate.
Andrey Logvin844125c2020-11-18 22:07:15 +0000181 """
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100182 headers = _CreateHeaders(_GenerateOauthToken())
183 http = httplib2.Http()
Andrey Logvin844125c2020-11-18 22:07:15 +0000184
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100185 response, content = http.request(url + '/uploads/' + upload_token +
186 '?additional_info=measurements',
187 method='GET',
188 headers=headers)
Andrey Logvin844125c2020-11-18 22:07:15 +0000189
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100190 if response.status != 200:
191 print('Failed to reach the dashboard to get full upload info.')
Andrey Logvin844125c2020-11-18 22:07:15 +0000192 return False
193
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100194 resp_json = json.loads(content)
195 print('Full upload info: %s.' % json.dumps(resp_json, indent=4))
196
197 if 'measurements' in resp_json:
198 measurements_cnt = len(resp_json['measurements'])
199 not_completed_state_cnt = len(
200 [m for m in resp_json['measurements'] if m['state'] != 'COMPLETED'])
201
202 if (measurements_cnt >= min_measurements_amount
203 and (not_completed_state_cnt /
204 (measurements_cnt * 1.0) <= max_failed_measurements_percent)):
205 print(('Not all measurements were confirmed to upload. '
206 'Measurements count: %d, failed to upload or timed out: %d' %
207 (measurements_cnt, not_completed_state_cnt)))
208 return True
209
210 return False
211
Andrey Logvin844125c2020-11-18 22:07:15 +0000212
Patrik Höglund457c8cf2020-03-13 14:43:21 +0100213# TODO(https://crbug.com/1029452): HACKHACK
Andrey Logvinb6b678d2020-11-25 10:33:58 +0000214# Remove once we have doubles in the proto and handle -infinity correctly.
Patrik Höglunda89ad612020-03-13 16:08:08 +0100215def _ApplyHacks(dicts):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100216 def _NoInf(value):
217 if value == float('inf'):
218 return histogram.JS_MAX_VALUE
219 if value == float('-inf'):
220 return -histogram.JS_MAX_VALUE
221 return value
Andrey Logvin659d7012020-11-24 15:12:25 +0000222
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100223 for d in dicts:
224 if 'running' in d:
225 d['running'] = [_NoInf(value) for value in d['running']]
226 if 'sampleValues' in d:
227 d['sampleValues'] = [_NoInf(value) for value in d['sampleValues']]
Mirko Bonadei8cc66952020-10-30 10:13:45 +0100228
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100229 return dicts
Patrik Höglund457c8cf2020-03-13 14:43:21 +0100230
231
Patrik Höglund0569a122020-03-13 12:26:42 +0100232def _LoadHistogramSetFromProto(options):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100233 hs = histogram_set.HistogramSet()
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100234 with open(options.input_results_file, 'rb') as f:
Christoffer Janssonc98fb2c2022-02-08 21:43:45 +0100235 hs.ImportProto(f.read())
Patrik Höglund0569a122020-03-13 12:26:42 +0100236
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100237 return hs
Patrik Höglund0569a122020-03-13 12:26:42 +0100238
239
240def _AddBuildInfo(histograms, options):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100241 common_diagnostics = {
242 reserved_infos.MASTERS: options.perf_dashboard_machine_group,
243 reserved_infos.BOTS: options.bot,
244 reserved_infos.POINT_ID: options.commit_position,
245 reserved_infos.BENCHMARKS: options.test_suite,
246 reserved_infos.WEBRTC_REVISIONS: str(options.webrtc_git_hash),
247 reserved_infos.BUILD_URLS: options.build_page_url,
248 }
Patrik Höglund0569a122020-03-13 12:26:42 +0100249
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100250 for k, v in list(common_diagnostics.items()):
251 histograms.AddSharedDiagnosticToAllHistograms(k.name,
252 generic_set.GenericSet([v]))
Patrik Höglund0569a122020-03-13 12:26:42 +0100253
254
255def _DumpOutput(histograms, output_file):
Jeremy Lecontefa577c52022-03-14 20:06:11 +0100256 with open(output_file, 'w') as f:
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100257 json.dump(_ApplyHacks(histograms.AsDicts()), f, indent=4)
Patrik Höglund0569a122020-03-13 12:26:42 +0100258
259
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100260def UploadToDashboardImpl(options):
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100261 histograms = _LoadHistogramSetFromProto(options)
262 _AddBuildInfo(histograms, options)
Patrik Höglund0569a122020-03-13 12:26:42 +0100263
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100264 if options.output_json_file:
265 _DumpOutput(histograms, options.output_json_file)
Patrik Höglund0569a122020-03-13 12:26:42 +0100266
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100267 response, content = _SendHistogramSet(options.dashboard_url, histograms)
Patrik Höglund0569a122020-03-13 12:26:42 +0100268
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100269 if response.status != 200:
270 print(('Upload failed with %d: %s\n\n%s' %
271 (response.status, response.reason, content)))
Andrey Logvin728b5d02020-11-11 17:16:26 +0000272 return 1
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100273
274 upload_token = json.loads(content).get('token')
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100275 if not upload_token:
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100276 print(('Received 200 from dashboard. ',
277 'Not waiting for the upload status confirmation.'))
278 return 0
279
280 response, resp_json = _WaitForUploadConfirmation(
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100281 options.dashboard_url, upload_token, options.wait_timeout_sec,
282 options.wait_polling_period_sec)
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100283
284 if ((resp_json and resp_json['state'] == 'COMPLETED')
285 or _CheckFullUploadInfo(options.dashboard_url, upload_token)):
286 print('Upload completed.')
287 return 0
288
289 if response.status != 200:
290 print(('Upload status poll failed with %d: %s' %
291 (response.status, response.reason)))
292 return 1
293
294 if resp_json['state'] == 'FAILED':
295 print('Upload failed.')
296 return 1
297
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100298 print(('Upload wasn\'t completed in a given time: %s seconds.' %
Christoffer Jansson4e8a7732022-02-08 09:01:12 +0100299 options.wait_timeout_sec))
300 return 1
Jeremy Leconte2c4a4472022-03-14 15:22:37 +0100301
302
303def UploadToDashboard(options):
304 try:
305 exit_code = UploadToDashboardImpl(options)
306 except RuntimeError as e:
307 print(e)
308 return 2
309 return exit_code