blob: 84e5280ab7bf1c8b96c87aaef8241b420cb62b11 [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2014 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.
"""A tool that uploads data to the performance dashboard."""
import argparse
import httplib
import json
import pprint
import re
import sys
import urllib
import urllib2
# TODO(yzshen): The following are missing currently:
# (1) CL range on the dashboard;
# (2) improvement direction on the dashboard;
# (3) a link from the build step pointing to the dashboard page.
_PERF_LINE_FORMAT = r"""^\s*([^\s/]+) # chart name
(/([^\s/]+))? # trace name (optional, separated with
# the chart name by a '/')
\s+(\S+) # value
\s+(\S+) # units
\s*$"""
_PRODUCTION_SERVER = "https://chromeperf.appspot.com"
_TESTING_SERVER = "https://chrome-perf.googleplex.com"
def UploadPerfData(master_name, perf_id, test_name, builder_name, build_number,
revision, perf_data, point_id, dry_run=False,
testing_dashboard=True):
"""Uploads perf data.
Args:
Please see the help for command-line args.
Returns:
A boolean value indicating whether the operation succeeded or not.
"""
def _ConvertToUploadFormat():
"""Converts perf data to the format that the server understands.
Returns:
A dictionary that (after being converted to JSON) conforms to the server
format.
"""
charts = {}
line_format = re.compile(_PERF_LINE_FORMAT, re.VERBOSE)
for line in perf_data:
match = re.match(line_format, line)
assert match, "Unable to parse the following input: %s" % line
chart_name = match.group(1)
trace_name = match.group(3) if match.group(3) else "summary"
if chart_name not in charts:
charts[chart_name] = {}
charts[chart_name][trace_name] = {
"type": "scalar",
"value": float(match.group(4)),
"units": match.group(5)
}
return {
"master": master_name,
"bot": perf_id,
"masterid": master_name,
"buildername": builder_name,
"buildnumber": build_number,
"versions": {
"mojo": revision
},
"point_id": point_id,
"supplemental": {},
"chart_data": {
"format_version": "1.0",
"benchmark_name": test_name,
"charts": charts
}
}
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)
formatted_data = _ConvertToUploadFormat()
server_url = _TESTING_SERVER if testing_dashboard else _PRODUCTION_SERVER
if dry_run:
print "Won't upload because --dry-run is specified."
print "Server: %s" % server_url
print "Data:"
pprint.pprint(formatted_data)
else:
print "Uploading data to %s ..." % server_url
try:
_Upload(server_url, json.dumps(formatted_data))
except _UploadException as e:
print e
return False
print "Done."
dashboard_params = urllib.urlencode({
"masters": master_name,
"bots": perf_id,
"tests": test_name,
"rev": point_id
})
print "Results Dashboard: %s/report?%s" % (server_url, dashboard_params)
return True
def main():
parser = argparse.ArgumentParser(
description="A tool that uploads data to the performance dashboard.")
parser.add_argument(
"--master-name", required=True,
help="Buildbot master name, used to construct link to buildbot log by "
"the dashboard, and also as the top-level category for the data.")
parser.add_argument(
"--perf-id", required=True,
help="Used as the second-level category for the data, usually the "
"platform type.")
parser.add_argument(
"--test-name", required=True,
help="Name of the test that the perf data was generated from.")
parser.add_argument(
"--builder-name", required=True,
help="Buildbot builder name, used to construct link to buildbot log by "
"the dashboard.")
parser.add_argument(
"--build-number", required=True, type=int,
help="Build number, used to construct link to buildbot log by the "
"dashboard.")
parser.add_argument(
"--revision", required=True, help="The mojo git commit hash.")
parser.add_argument(
"--perf-data", required=True, metavar="foo_perf.log",
type=argparse.FileType("r"),
help="A text file containing the perf data. Each line is a data point in "
"the following format: chart_name[/trace_name] value units")
parser.add_argument(
"--point-id", required=True, type=int,
help="The x coordinate for the data points.")
parser.add_argument(
"--dry-run", action="store_true",
help="Display the server URL and the data to upload, but not actually "
"upload the data.")
server_group = parser.add_mutually_exclusive_group()
server_group.add_argument(
"--testing-dashboard", action="store_true", default=True,
help="Upload the data to the testing dashboard (default).")
server_group.add_argument(
"--production-dashboard", dest="testing_dashboard", action="store_false",
default=False, help="Upload the data to the production dashboard.")
args = parser.parse_args()
result = UploadPerfData(args.master_name, args.perf_id, args.test_name,
args.builder_name, args.build_number, args.revision,
args.perf_data, args.point_id, args.dry_run,
args.testing_dashboard)
return 0 if result else 1
if __name__ == '__main__':
sys.exit(main())