| # 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 |