#!/usr/bin/env python3 # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. import argparse from functools import partial, reduce import json import operator import os import random import string from thclient import (TreeherderClient, TreeherderResultSetCollection, TreeherderJobCollection) import time from runner import format_result_summary def geometric_mean(iterable): filtered = list(filter(lambda x: x > 0, iterable)) return (reduce(operator.mul, filtered)) ** (1.0 / len(filtered)) def format_testcase_name(name): temp = name.replace('http://localhost:8000/page_load_test/', '') temp = temp.replace('http://localhost:8000/tp6/', '') temp = temp.split('/')[0] temp = temp[0:80] return temp def format_perf_data(perf_json, engine='servo'): suites = [] measurement = "domComplete" # Change this to an array when we have more def get_time_from_nav_start(timings, measurement): return timings[measurement] - timings['navigationStart'] measurementFromNavStart = partial(get_time_from_nav_start, measurement=measurement) if (engine == 'gecko'): name = 'gecko.{}'.format(measurement) else: name = measurement suite = { "name": name, "value": geometric_mean(map(measurementFromNavStart, perf_json)), "subtests": [] } for testcase in perf_json: if measurementFromNavStart(testcase) < 0: value = -1 # print('Error: test case has negative timing. Test timeout?') else: value = measurementFromNavStart(testcase) suite["subtests"].append({ "name": format_testcase_name(testcase["testcase"]), "value": value }) suites.append(suite) return { "performance_data": { # https://bugzilla.mozilla.org/show_bug.cgi?id=1271472 "framework": {"name": "servo-perf"}, "suites": suites } } def create_resultset_collection(dataset): print("[DEBUG] ResultSet Collection:") print(dataset) trsc = TreeherderResultSetCollection() for data in dataset: trs = trsc.get_resultset() trs.add_push_timestamp(data['push_timestamp']) trs.add_revision(data['revision']) trs.add_author(data['author']) # TODO: figure out where type is used # trs.add_type(data['type']) revisions = [] for rev in data['revisions']: tr = trs.get_revision() tr.add_revision(rev['revision']) tr.add_author(rev['author']) tr.add_comment(rev['comment']) tr.add_repository(rev['repository']) revisions.append(tr) trs.add_revisions(revisions) trsc.add(trs) return trsc def create_job_collection(dataset): print("[DEBUG] Job Collection:") print(dataset) tjc = TreeherderJobCollection() for data in dataset: tj = tjc.get_job() tj.add_revision(data['revision']) tj.add_project(data['project']) tj.add_coalesced_guid(data['job']['coalesced']) tj.add_job_guid(data['job']['job_guid']) tj.add_job_name(data['job']['name']) tj.add_job_symbol(data['job']['job_symbol']) tj.add_group_name(data['job']['group_name']) tj.add_group_symbol(data['job']['group_symbol']) tj.add_description(data['job']['desc']) tj.add_product_name(data['job']['product_name']) tj.add_state(data['job']['state']) tj.add_result(data['job']['result']) tj.add_reason(data['job']['reason']) tj.add_who(data['job']['who']) tj.add_tier(data['job']['tier']) tj.add_submit_timestamp(data['job']['submit_timestamp']) tj.add_start_timestamp(data['job']['start_timestamp']) tj.add_end_timestamp(data['job']['end_timestamp']) tj.add_machine(data['job']['machine']) tj.add_build_info( data['job']['build_platform']['os_name'], data['job']['build_platform']['platform'], data['job']['build_platform']['architecture'] ) tj.add_machine_info( data['job']['machine_platform']['os_name'], data['job']['machine_platform']['platform'], data['job']['machine_platform']['architecture'] ) tj.add_option_collection(data['job']['option_collection']) for artifact_data in data['job']['artifacts']: tj.add_artifact( artifact_data['name'], artifact_data['type'], artifact_data['blob'] ) tjc.add(tj) return tjc # TODO: refactor this big function to smaller chunks def submit(perf_data, failures, revision, summary, engine): print("[DEBUG] failures:") print(list(map(lambda x: x['testcase'], failures))) author = "{} <{}>".format(revision['author']['name'], revision['author']['email']) dataset = [ { # The top-most revision in the list of commits for a push. 'revision': revision['commit'], 'author': author, 'push_timestamp': int(revision['author']['timestamp']), 'type': 'push', # a list of revisions associated with the resultset. There should # be at least one. 'revisions': [ { 'comment': revision['subject'], 'revision': revision['commit'], 'repository': 'servo', 'author': author } ] } ] trsc = create_resultset_collection(dataset) result = "success" # TODO: verify a failed test won't affect Perfherder visualization # if len(failures) > 0: # result = "testfailed" hashlen = len(revision['commit']) job_guid = ''.join( random.choice(string.ascii_letters + string.digits) for i in range(hashlen) ) if (engine == "gecko"): project = "servo" job_symbol = 'PLG' group_symbol = 'SPG' group_name = 'Servo Perf on Gecko' else: project = "servo" job_symbol = 'PL' group_symbol = 'SP' group_name = 'Servo Perf' dataset = [ { 'project': project, 'revision': revision['commit'], 'job': { 'job_guid': job_guid, 'product_name': project, 'reason': 'scheduler', # TODO: What is `who` for? 'who': 'Servo', 'desc': 'Servo Page Load Time Tests', 'name': 'Servo Page Load Time', # The symbol representing the job displayed in # treeherder.allizom.org 'job_symbol': job_symbol, # The symbol representing the job group in # treeherder.allizom.org 'group_symbol': group_symbol, 'group_name': group_name, # TODO: get the real timing from the test runner 'submit_timestamp': str(int(time.time())), 'start_timestamp': str(int(time.time())), 'end_timestamp': str(int(time.time())), 'state': 'completed', 'result': result, # "success" or "testfailed" 'machine': 'local-machine', # TODO: read platform from test result 'build_platform': { 'platform': 'linux64', 'os_name': 'linux', 'architecture': 'x86_64' }, 'machine_platform': { 'platform': 'linux64', 'os_name': 'linux', 'architecture': 'x86_64' }, 'option_collection': {'opt': True}, # jobs can belong to different tiers # setting the tier here will determine which tier the job # belongs to. However, if a job is set as Tier of 1, but # belongs to the Tier 2 profile on the server, it will still # be saved as Tier 2. 'tier': 1, # the ``name`` of the log can be the default of "buildbot_text" # however, you can use a custom name. See below. # TODO: point this to the log when we have them uploaded to S3 'log_references': [ { 'url': 'TBD', 'name': 'test log' } ], # The artifact can contain any kind of structured data # associated with a test. 'artifacts': [ { 'type': 'json', 'name': 'performance_data', # TODO: include the job_guid when the runner actually # generates one # 'job_guid': job_guid, 'blob': perf_data }, { 'type': 'json', 'name': 'Job Info', # 'job_guid': job_guid, "blob": { "job_details": [ { "content_type": "raw_html", "title": "Result Summary", "value": summary } ] } } ], # List of job guids that were coalesced to this job 'coalesced': [] } } ] tjc = create_job_collection(dataset) # TODO: extract this read credential code out of this function. cred = { 'client_id': os.environ['TREEHERDER_CLIENT_ID'], 'secret': os.environ['TREEHERDER_CLIENT_SECRET'] } client = TreeherderClient(server_url='https://treeherder.mozilla.org', client_id=cred['client_id'], secret=cred['secret']) # data structure validation is automatically performed here, if validation # fails a TreeherderClientError is raised client.post_collection('servo', trsc) client.post_collection('servo', tjc) def main(): parser = argparse.ArgumentParser( description=("Submit Servo performance data to Perfherder. " "Remember to set your Treeherder credential as environment" " variable \'TREEHERDER_CLIENT_ID\' and " "\'TREEHERDER_CLIENT_SECRET\'")) parser.add_argument("perf_json", help="the output json from runner") parser.add_argument("revision_json", help="the json containing the servo revision data") parser.add_argument("--engine", type=str, default='servo', help=("The engine to run the tests on. Currently only" " servo and gecko are supported.")) args = parser.parse_args() with open(args.perf_json, 'r') as f: result_json = json.load(f) with open(args.revision_json, 'r') as f: revision = json.load(f) perf_data = format_perf_data(result_json, args.engine) failures = list(filter(lambda x: x['domComplete'] == -1, result_json)) summary = format_result_summary(result_json).replace('\n', '
') submit(perf_data, failures, revision, summary, args.engine) print("Done!") if __name__ == "__main__": main()