blob: d5388c352355460b20ee238ffeb367f9ca2fef16 [file] [log] [blame]
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Disable the line-too-long warning.
# pylint: disable=C0301
"""This module implements the Chromium Performance Dashboard JSON v1.0 data
format.
See http://www.chromium.org/developers/speed-infra/performance-dashboard/sending-data-to-the-performance-dashboard.
"""
import httplib
import json
import pprint
import subprocess
import urllib
import urllib2
_LOCAL_SERVER = "http://127.0.0.1:8080"
class ChartDataRecorder(object):
"""Allows one to record measurement values one by one and then generate the
JSON string that represents them in the 'chart_data' format expected by the
performance dashboard.
"""
def __init__(self, benchmark_name):
self.charts = {}
self.benchmark_name = benchmark_name
def record_scalar(self, chart_name, value_name, units, value):
"""Records a single measurement value of a scalar type."""
if chart_name not in self.charts:
self.charts[chart_name] = {}
self.charts[chart_name][value_name] = {
'type': 'scalar',
'units': units,
'value': value}
def record_vector(self, chart_name, value_name, units, values):
"""Records a single measurement value of a list of scalars type."""
if chart_name not in self.charts:
self.charts[chart_name] = {}
self.charts[chart_name][value_name] = {
'type': 'list_of_scalar_values',
'units': units,
'values': values}
def get_chart_data(self):
"""Returns the JSON string representing the recorded chart data, wrapping
it with the required meta data."""
chart_data = {
'format_version': '1.0',
'benchmark_name': self.benchmark_name,
'charts': self.charts
}
return chart_data
def add_argparse_server_arguments(parser):
"""Adds argparse arguments needed to upload the chart data to a performance
dashboard to the given parser.
"""
dashboard_group = parser.add_argument_group('Performance dashboard server',
'These arguments allow to specify the performance dashboard server '
'to upload the results to.')
dashboard_group.add_argument(
'--upload', action='store_true',
help='Upload the results to performance dashboard. Further arguments '
'in this group are relevant only if --upload is passed.')
dashboard_group.add_argument(
'--server-url',
help='Url of the server instance to upload the results to. By default a '
'local instance is assumed to be running on port 8080.')
dashboard_group.add_argument(
'--master-name',
help='Buildbot master name, used to construct link to buildbot log by '
'the dashboard, and also as the top-level category for the data.')
dashboard_group.add_argument(
'--bot-name',
help='Used as the second-level category for the data.')
dashboard_group.add_argument(
'--test-name',
help='Name of the test that the perf data was generated from.')
dashboard_group.add_argument(
'--builder-name',
help='Buildbot builder name, used to construct link to buildbot log by '
'the dashboard.')
dashboard_group.add_argument(
'--build-number', type=int,
help='Build number, used to construct link to buildbot log by the '
'dashboard.')
dashboard_group.add_argument(
'--dry-run', action='store_true', default=False,
help='Display the server URL and the data to upload, but do not actually '
'upload the data.')
def normalize_label(label):
"""Normalizes a label to be used for data sent to performance dashboard.
This replaces:
'/' -> '-', as slashes are used to denote test/sub-test relation.
' ' -> '_', as there is a convention of not using spaces in test names.
Returns:
Normalized label.
"""
return label.replace('/', '-').replace(' ', '_')
def _get_commit_count():
"""Returns the number of git commits in the repository of the cwd."""
return subprocess.check_output(
['git', 'rev-list', 'HEAD', '--count']).strip()
def _get_current_commit():
"""Returns the hash of the current commit in the repository of the cwd."""
return subprocess.check_output(["git", "rev-parse", "HEAD"]).strip()
class _UploadException(Exception):
pass
def _upload(server_url, json_data):
"""Make an HTTP POST with the given data to the performance dashboard.
Args:
server_url: URL of the performance dashboard instance.
json_data: JSON string that contains the data to be sent.
Raises:
_UploadException: An error occurred during uploading.
"""
# When data is provided to urllib2.Request, a POST is sent instead of GET.
# The data must be in the application/x-www-form-urlencoded format.
data = urllib.urlencode({"data": json_data})
req = urllib2.Request("%s/add_point" % server_url, data)
try:
urllib2.urlopen(req)
except urllib2.HTTPError as e:
raise _UploadException('HTTPError: %d. Response: %s\n'
'JSON: %s\n' % (e.code, e.read(), json_data))
except urllib2.URLError as e:
raise _UploadException('URLError: %s for JSON %s\n' %
(str(e.reason), json_data))
except httplib.HTTPException as e:
raise _UploadException('HTTPException for JSON %s\n' % json_data)
def upload_chart_data(master_name, bot_name, test_name, builder_name,
build_number, chart_data, server_url=None, dry_run=False):
"""Uploads the provided chart data to an instance of performance dashboard.
See the argparse help above for description of the arguments.
Returns:
A boolean value indicating whether the operation succeeded or not.
"""
if (not master_name or not bot_name or not test_name or not builder_name or
not build_number):
print ('Cannot upload perf data to the dashboard because not all of the '
'following values are specified: master-name, bot-name, test_name, '
'builder-name, build-number.')
return False
point_id = _get_commit_count()
cur_commit = _get_current_commit()
# Wrap the |chart_data| with meta data as required by the spec.
formatted_data = {
'master': master_name,
'bot': bot_name,
'masterid': master_name,
'buildername': builder_name,
'buildnumber': build_number,
'versions': {
'mojo': cur_commit,
},
'point_id': point_id,
'supplemental': {},
'chart_data': chart_data,
}
upload_url = server_url if server_url else _LOCAL_SERVER
if dry_run:
print 'Will not upload because --dry-run is specified.'
print 'Server: %s' % upload_url
print 'Data:'
pprint.pprint(formatted_data)
else:
print 'Uploading data to %s ...' % upload_url
try:
_upload(upload_url, json.dumps(formatted_data))
except _UploadException as e:
print e
return False
print "Done."
dashboard_params = urllib.urlencode({
'masters': master_name,
'bots': bot_name,
'tests': test_name,
'rev': point_id
})
print 'Results Dashboard: %s/report?%s' % (upload_url, dashboard_params)
return True