Path: blob/main_old/scripts/process_angle_perf_results.py
1693 views
#!/usr/bin/env vpython1#2# Copyright 2021 The ANGLE Project Authors. All rights reserved.3# Use of this source code is governed by a BSD-style license that can be4# found in the LICENSE file.5#6# process_angle_perf_results.py:7# Perf result merging and upload. Adapted from the Chromium script:8# https://chromium.googlesource.com/chromium/src/+/main/tools/perf/process_perf_results.py910from __future__ import print_function1112import argparse13import collections14import json15import logging16import multiprocessing17import os18import shutil19import sys20import tempfile21import time22import uuid2324logging.basicConfig(25level=logging.INFO,26format='(%(levelname)s) %(asctime)s pid=%(process)d'27' %(module)s.%(funcName)s:%(lineno)d %(message)s')2829d = os.path.dirname30ANGLE_DIR = d(d(os.path.realpath(__file__)))31sys.path.append(os.path.join(ANGLE_DIR, 'tools', 'perf'))32import cross_device_test_config3334from core import path_util3536path_util.AddTelemetryToPath()37from core import upload_results_to_perf_dashboard38from core import bot_platforms39from core import results_merger4041path_util.AddAndroidPylibToPath()42try:43from pylib.utils import logdog_helper44except ImportError:45pass4647path_util.AddTracingToPath()48from tracing.value import histogram49from tracing.value import histogram_set50from tracing.value.diagnostics import generic_set51from tracing.value.diagnostics import reserved_infos5253RESULTS_URL = 'https://chromeperf.appspot.com'54JSON_CONTENT_TYPE = 'application/json'55MACHINE_GROUP = 'ANGLE'56BUILD_URL = 'https://ci.chromium.org/ui/p/angle/builders/ci/%s/%d'575859def _upload_perf_results(json_to_upload, name, configuration_name, build_properties,60output_json_file):61"""Upload the contents of result JSON(s) to the perf dashboard."""62args = [63'--buildername',64build_properties['buildername'],65'--buildnumber',66build_properties['buildnumber'],67'--name',68name,69'--configuration-name',70configuration_name,71'--results-file',72json_to_upload,73'--results-url',74RESULTS_URL,75'--output-json-file',76output_json_file,77'--perf-dashboard-machine-group',78MACHINE_GROUP,79'--got-angle-revision',80build_properties['got_angle_revision'],81'--send-as-histograms',82'--project',83'angle',84]8586if build_properties.get('git_revision'):87args.append('--git-revision')88args.append(build_properties['git_revision'])8990#TODO(crbug.com/1072729): log this in top level91logging.info('upload_results_to_perf_dashboard: %s.' % args)9293return upload_results_to_perf_dashboard.main(args)949596def _merge_json_output(output_json, jsons_to_merge, extra_links, test_cross_device=False):97"""Merges the contents of one or more results JSONs.9899Args:100output_json: A path to a JSON file to which the merged results should be101written.102jsons_to_merge: A list of JSON files that should be merged.103extra_links: a (key, value) map in which keys are the human-readable strings104which describe the data, and value is logdog url that contain the data.105"""106begin_time = time.time()107merged_results = results_merger.merge_test_results(jsons_to_merge, test_cross_device)108109# Only append the perf results links if present110if extra_links:111merged_results['links'] = extra_links112113with open(output_json, 'w') as f:114json.dump(merged_results, f)115116end_time = time.time()117print_duration('Merging json test results', begin_time, end_time)118return 0119120121def _handle_perf_json_test_results(benchmark_directory_map, test_results_list):122"""Checks the test_results.json under each folder:1231241. mark the benchmark 'enabled' if tests results are found1252. add the json content to a list for non-ref.126"""127begin_time = time.time()128benchmark_enabled_map = {}129for benchmark_name, directories in benchmark_directory_map.items():130for directory in directories:131# Obtain the test name we are running132is_ref = '.reference' in benchmark_name133enabled = True134try:135with open(os.path.join(directory, 'test_results.json')) as json_data:136json_results = json.load(json_data)137if not json_results:138# Output is null meaning the test didn't produce any results.139# Want to output an error and continue loading the rest of the140# test results.141logging.warning('No results produced for %s, skipping upload' % directory)142continue143if json_results.get('version') == 3:144# Non-telemetry tests don't have written json results but145# if they are executing then they are enabled and will generate146# chartjson results.147if not bool(json_results.get('tests')):148enabled = False149if not is_ref:150# We don't need to upload reference build data to the151# flakiness dashboard since we don't monitor the ref build152test_results_list.append(json_results)153except IOError as e:154# TODO(crbug.com/936602): Figure out how to surface these errors. Should155# we have a non-zero exit code if we error out?156logging.error('Failed to obtain test results for %s: %s', benchmark_name, e)157continue158if not enabled:159# We don't upload disabled benchmarks or tests that are run160# as a smoke test161logging.info('Benchmark %s ran no tests on at least one shard' % benchmark_name)162continue163benchmark_enabled_map[benchmark_name] = True164165end_time = time.time()166print_duration('Analyzing perf json test results', begin_time, end_time)167return benchmark_enabled_map168169170def _generate_unique_logdog_filename(name_prefix):171return name_prefix + '_' + str(uuid.uuid4())172173174def _handle_perf_logs(benchmark_directory_map, extra_links):175""" Upload benchmark logs to logdog and add a page entry for them. """176begin_time = time.time()177benchmark_logs_links = collections.defaultdict(list)178179for benchmark_name, directories in benchmark_directory_map.items():180for directory in directories:181benchmark_log_file = os.path.join(directory, 'benchmark_log.txt')182if os.path.exists(benchmark_log_file):183with open(benchmark_log_file) as f:184uploaded_link = logdog_helper.text(185name=_generate_unique_logdog_filename(benchmark_name), data=f.read())186benchmark_logs_links[benchmark_name].append(uploaded_link)187188logdog_file_name = _generate_unique_logdog_filename('Benchmarks_Logs')189logdog_stream = logdog_helper.text(190logdog_file_name,191json.dumps(benchmark_logs_links, sort_keys=True, indent=4, separators=(',', ': ')),192content_type=JSON_CONTENT_TYPE)193extra_links['Benchmarks logs'] = logdog_stream194end_time = time.time()195print_duration('Generating perf log streams', begin_time, end_time)196197198def _handle_benchmarks_shard_map(benchmarks_shard_map_file, extra_links):199begin_time = time.time()200with open(benchmarks_shard_map_file) as f:201benchmarks_shard_data = f.read()202logdog_file_name = _generate_unique_logdog_filename('Benchmarks_Shard_Map')203logdog_stream = logdog_helper.text(204logdog_file_name, benchmarks_shard_data, content_type=JSON_CONTENT_TYPE)205extra_links['Benchmarks shard map'] = logdog_stream206end_time = time.time()207print_duration('Generating benchmark shard map stream', begin_time, end_time)208209210def _get_benchmark_name(directory):211return os.path.basename(directory).replace(" benchmark", "")212213214def _scan_output_dir(task_output_dir):215benchmark_directory_map = {}216benchmarks_shard_map_file = None217218directory_list = [219f for f in os.listdir(task_output_dir)220if not os.path.isfile(os.path.join(task_output_dir, f))221]222benchmark_directory_list = []223for directory in directory_list:224for f in os.listdir(os.path.join(task_output_dir, directory)):225path = os.path.join(task_output_dir, directory, f)226if os.path.isdir(path):227benchmark_directory_list.append(path)228elif path.endswith('benchmarks_shard_map.json'):229benchmarks_shard_map_file = path230# Now create a map of benchmark name to the list of directories231# the lists were written to.232for directory in benchmark_directory_list:233benchmark_name = _get_benchmark_name(directory)234if benchmark_name in benchmark_directory_map.keys():235benchmark_directory_map[benchmark_name].append(directory)236else:237benchmark_directory_map[benchmark_name] = [directory]238239return benchmark_directory_map, benchmarks_shard_map_file240241242def process_perf_results(output_json,243configuration_name,244build_properties,245task_output_dir,246smoke_test_mode,247output_results_dir,248lightweight=False,249skip_perf=False):250"""Process perf results.251252Consists of merging the json-test-format output, uploading the perf test253output (histogram), and store the benchmark logs in logdog.254255Each directory in the task_output_dir represents one benchmark256that was run. Within this directory, there is a subdirectory with the name257of the benchmark that was run. In that subdirectory, there is a258perftest-output.json file containing the performance results in histogram259format and an output.json file containing the json test results for the260benchmark.261262Returns:263(return_code, upload_results_map):264return_code is 0 if the whole operation is successful, non zero otherwise.265benchmark_upload_result_map: the dictionary that describe which benchmarks266were successfully uploaded.267"""268handle_perf = not lightweight or not skip_perf269handle_non_perf = not lightweight or skip_perf270logging.info('lightweight mode: %r; handle_perf: %r; handle_non_perf: %r' %271(lightweight, handle_perf, handle_non_perf))272273begin_time = time.time()274return_code = 0275benchmark_upload_result_map = {}276277benchmark_directory_map, benchmarks_shard_map_file = _scan_output_dir(task_output_dir)278279test_results_list = []280extra_links = {}281282if handle_non_perf:283# First, upload benchmarks shard map to logdog and add a page284# entry for it in extra_links.285if benchmarks_shard_map_file:286_handle_benchmarks_shard_map(benchmarks_shard_map_file, extra_links)287288# Second, upload all the benchmark logs to logdog and add a page entry for289# those links in extra_links.290_handle_perf_logs(benchmark_directory_map, extra_links)291292# Then try to obtain the list of json test results to merge293# and determine the status of each benchmark.294benchmark_enabled_map = _handle_perf_json_test_results(benchmark_directory_map,295test_results_list)296297build_properties_map = json.loads(build_properties)298if not configuration_name:299# we are deprecating perf-id crbug.com/817823300configuration_name = build_properties_map['buildername']301302_update_perf_results_for_calibration(benchmarks_shard_map_file, benchmark_enabled_map,303benchmark_directory_map, configuration_name)304if not smoke_test_mode and handle_perf:305try:306return_code, benchmark_upload_result_map = _handle_perf_results(307benchmark_enabled_map, benchmark_directory_map, configuration_name,308build_properties_map, extra_links, output_results_dir)309except Exception:310logging.exception('Error handling perf results jsons')311return_code = 1312313if handle_non_perf:314# Finally, merge all test results json, add the extra links and write out to315# output location316try:317_merge_json_output(output_json, test_results_list, extra_links,318configuration_name in cross_device_test_config.TARGET_DEVICES)319except Exception:320logging.exception('Error handling test results jsons.')321322end_time = time.time()323print_duration('Total process_perf_results', begin_time, end_time)324return return_code, benchmark_upload_result_map325326327def _merge_histogram_results(histogram_lists):328merged_results = []329for histogram_list in histogram_lists:330merged_results += histogram_list331332return merged_results333334335def _load_histogram_set_from_dict(data):336histograms = histogram_set.HistogramSet()337histograms.ImportDicts(data)338return histograms339340341def _add_build_info(results, benchmark_name, build_properties):342histograms = _load_histogram_set_from_dict(results)343344common_diagnostics = {345reserved_infos.MASTERS:346build_properties['builder_group'],347reserved_infos.BOTS:348build_properties['buildername'],349reserved_infos.POINT_ID:350build_properties['angle_commit_pos'],351reserved_infos.BENCHMARKS:352benchmark_name,353reserved_infos.ANGLE_REVISIONS:354build_properties['got_angle_revision'],355reserved_infos.BUILD_URLS:356BUILD_URL % (build_properties['buildername'], build_properties['buildnumber']),357}358359for k, v in common_diagnostics.items():360histograms.AddSharedDiagnosticToAllHistograms(k.name, generic_set.GenericSet([v]))361362return histograms.AsDicts()363364365def _merge_perf_results(benchmark_name, results_filename, directories, build_properties):366begin_time = time.time()367collected_results = []368for directory in directories:369filename = os.path.join(directory, 'perf_results.json')370try:371with open(filename) as pf:372collected_results.append(json.load(pf))373except IOError as e:374# TODO(crbug.com/936602): Figure out how to surface these errors. Should375# we have a non-zero exit code if we error out?376logging.error('Failed to obtain perf results from %s: %s', directory, e)377if not collected_results:378logging.error('Failed to obtain any perf results from %s.', benchmark_name)379return380381# Assuming that multiple shards will be histogram set382# Non-telemetry benchmarks only ever run on one shard383merged_results = []384assert (isinstance(collected_results[0], list))385merged_results = _merge_histogram_results(collected_results)386387# Write additional histogram build info.388merged_results = _add_build_info(merged_results, benchmark_name, build_properties)389390with open(results_filename, 'w') as rf:391json.dump(merged_results, rf)392393end_time = time.time()394print_duration(('%s results merging' % (benchmark_name)), begin_time, end_time)395396397def _upload_individual(benchmark_name, directories, configuration_name, build_properties,398output_json_file):399tmpfile_dir = tempfile.mkdtemp()400try:401upload_begin_time = time.time()402# There are potentially multiple directores with results, re-write and403# merge them if necessary404results_filename = None405if len(directories) > 1:406merge_perf_dir = os.path.join(os.path.abspath(tmpfile_dir), benchmark_name)407if not os.path.exists(merge_perf_dir):408os.makedirs(merge_perf_dir)409results_filename = os.path.join(merge_perf_dir, 'merged_perf_results.json')410_merge_perf_results(benchmark_name, results_filename, directories, build_properties)411else:412# It was only written to one shard, use that shards data413results_filename = os.path.join(directories[0], 'perf_results.json')414415results_size_in_mib = os.path.getsize(results_filename) / (2**20)416logging.info('Uploading perf results from %s benchmark (size %s Mib)' %417(benchmark_name, results_size_in_mib))418with open(output_json_file, 'w') as oj:419upload_return_code = _upload_perf_results(results_filename, benchmark_name,420configuration_name, build_properties, oj)421upload_end_time = time.time()422print_duration(('%s upload time' % (benchmark_name)), upload_begin_time,423upload_end_time)424return (benchmark_name, upload_return_code == 0)425finally:426shutil.rmtree(tmpfile_dir)427428429def _upload_individual_benchmark(params):430try:431return _upload_individual(*params)432except Exception:433benchmark_name = params[0]434upload_succeed = False435logging.exception('Error uploading perf result of %s' % benchmark_name)436return benchmark_name, upload_succeed437438439def _GetCpuCount(log=True):440try:441cpu_count = multiprocessing.cpu_count()442if sys.platform == 'win32':443# TODO(crbug.com/1190269) - we can't use more than 56444# cores on Windows or Python3 may hang.445cpu_count = min(cpu_count, 56)446return cpu_count447except NotImplementedError:448if log:449logging.warn('Failed to get a CPU count for this bot. See crbug.com/947035.')450# TODO(crbug.com/948281): This is currently set to 4 since the mac masters451# only have 4 cores. Once we move to all-linux, this can be increased or452# we can even delete this whole function and use multiprocessing.cpu_count()453# directly.454return 4455456457def _load_shard_id_from_test_results(directory):458shard_id = None459test_json_path = os.path.join(directory, 'test_results.json')460try:461with open(test_json_path) as f:462test_json = json.load(f)463all_results = test_json['tests']464for _, benchmark_results in all_results.items():465for _, measurement_result in benchmark_results.items():466shard_id = measurement_result['shard']467break468except IOError as e:469logging.error('Failed to open test_results.json from %s: %s', test_json_path, e)470except KeyError as e:471logging.error('Failed to locate results in test_results.json: %s', e)472return shard_id473474475def _find_device_id_by_shard_id(benchmarks_shard_map_file, shard_id):476try:477with open(benchmarks_shard_map_file) as f:478shard_map_json = json.load(f)479device_id = shard_map_json['extra_infos']['bot #%s' % shard_id]480except KeyError as e:481logging.error('Failed to locate device name in shard map: %s', e)482return device_id483484485def _update_perf_json_with_summary_on_device_id(directory, device_id):486perf_json_path = os.path.join(directory, 'perf_results.json')487try:488with open(perf_json_path, 'r') as f:489perf_json = json.load(f)490except IOError as e:491logging.error('Failed to open perf_results.json from %s: %s', perf_json_path, e)492summary_key_guid = str(uuid.uuid4())493summary_key_generic_set = {494'values': ['device_id'],495'guid': summary_key_guid,496'type': 'GenericSet'497}498perf_json.insert(0, summary_key_generic_set)499logging.info('Inserted summary key generic set for perf result in %s: %s', directory,500summary_key_generic_set)501stories_guids = set()502for entry in perf_json:503if 'diagnostics' in entry:504entry['diagnostics']['summaryKeys'] = summary_key_guid505stories_guids.add(entry['diagnostics']['stories'])506for entry in perf_json:507if 'guid' in entry and entry['guid'] in stories_guids:508entry['values'].append(device_id)509try:510with open(perf_json_path, 'w') as f:511json.dump(perf_json, f)512except IOError as e:513logging.error('Failed to writing perf_results.json to %s: %s', perf_json_path, e)514logging.info('Finished adding device id %s in perf result.', device_id)515516517def _should_add_device_id_in_perf_result(builder_name):518# We should always add device id in calibration builders.519# For testing purpose, adding fyi as well for faster turnaround, because520# calibration builders run every 24 hours.521return any([builder_name == p.name for p in bot_platforms.CALIBRATION_PLATFORMS522]) or (builder_name == 'android-pixel2-perf-fyi')523524525def _update_perf_results_for_calibration(benchmarks_shard_map_file, benchmark_enabled_map,526benchmark_directory_map, configuration_name):527if not _should_add_device_id_in_perf_result(configuration_name):528return529logging.info('Updating perf results for %s.', configuration_name)530for benchmark_name, directories in benchmark_directory_map.items():531if not benchmark_enabled_map.get(benchmark_name, False):532continue533for directory in directories:534shard_id = _load_shard_id_from_test_results(directory)535device_id = _find_device_id_by_shard_id(benchmarks_shard_map_file, shard_id)536_update_perf_json_with_summary_on_device_id(directory, device_id)537538539def _handle_perf_results(benchmark_enabled_map, benchmark_directory_map, configuration_name,540build_properties, extra_links, output_results_dir):541"""542Upload perf results to the perf dashboard.543544This method also upload the perf results to logdog and augment it to545|extra_links|.546547Returns:548(return_code, benchmark_upload_result_map)549return_code is 0 if this upload to perf dashboard successfully, 1550otherwise.551benchmark_upload_result_map is a dictionary describes which benchmark552was successfully uploaded.553"""554begin_time = time.time()555# Upload all eligible benchmarks to the perf dashboard556results_dict = {}557558invocations = []559for benchmark_name, directories in benchmark_directory_map.items():560if not benchmark_enabled_map.get(benchmark_name, False):561continue562# Create a place to write the perf results that you will write out to563# logdog.564output_json_file = os.path.join(output_results_dir, (str(uuid.uuid4()) + benchmark_name))565results_dict[benchmark_name] = output_json_file566#TODO(crbug.com/1072729): pass final arguments instead of build properties567# and configuration_name568invocations.append(569(benchmark_name, directories, configuration_name, build_properties, output_json_file))570571# Kick off the uploads in multiple processes572# crbug.com/1035930: We are hitting HTTP Response 429. Limit ourselves573# to 2 processes to avoid this error. Uncomment the following code once574# the problem is fixed on the dashboard side.575# pool = multiprocessing.Pool(_GetCpuCount())576pool = multiprocessing.Pool(2)577upload_result_timeout = False578try:579async_result = pool.map_async(_upload_individual_benchmark, invocations)580# TODO(crbug.com/947035): What timeout is reasonable?581results = async_result.get(timeout=4000)582except multiprocessing.TimeoutError:583upload_result_timeout = True584logging.error('Timeout uploading benchmarks to perf dashboard in parallel')585results = []586for benchmark_name in benchmark_directory_map:587results.append((benchmark_name, False))588finally:589pool.terminate()590591# Keep a mapping of benchmarks to their upload results592benchmark_upload_result_map = {}593for r in results:594benchmark_upload_result_map[r[0]] = r[1]595596logdog_dict = {}597upload_failures_counter = 0598logdog_stream = None599logdog_label = 'Results Dashboard'600for benchmark_name, output_file in results_dict.items():601upload_succeed = benchmark_upload_result_map[benchmark_name]602if not upload_succeed:603upload_failures_counter += 1604is_reference = '.reference' in benchmark_name605_write_perf_data_to_logfile(606benchmark_name,607output_file,608configuration_name,609build_properties,610logdog_dict,611is_reference,612upload_failure=not upload_succeed)613614logdog_file_name = _generate_unique_logdog_filename('Results_Dashboard_')615logdog_stream = logdog_helper.text(616logdog_file_name,617json.dumps(dict(logdog_dict), sort_keys=True, indent=4, separators=(',', ': ')),618content_type=JSON_CONTENT_TYPE)619if upload_failures_counter > 0:620logdog_label += (' %s merge script perf data upload failures' % upload_failures_counter)621extra_links[logdog_label] = logdog_stream622end_time = time.time()623print_duration('Uploading results to perf dashboard', begin_time, end_time)624if upload_result_timeout or upload_failures_counter > 0:625return 1, benchmark_upload_result_map626return 0, benchmark_upload_result_map627628629def _write_perf_data_to_logfile(benchmark_name, output_file, configuration_name, build_properties,630logdog_dict, is_ref, upload_failure):631viewer_url = None632# logdog file to write perf results to633if os.path.exists(output_file):634results = None635with open(output_file) as f:636try:637results = json.load(f)638except ValueError:639logging.error('Error parsing perf results JSON for benchmark %s' % benchmark_name)640if results:641try:642json_fname = _generate_unique_logdog_filename(benchmark_name)643output_json_file = logdog_helper.open_text(json_fname)644json.dump(results, output_json_file, indent=4, separators=(',', ': '))645except ValueError as e:646logging.error('ValueError: "%s" while dumping output to logdog' % e)647finally:648output_json_file.close()649viewer_url = output_json_file.get_viewer_url()650else:651logging.warning("Perf results JSON file doesn't exist for benchmark %s" % benchmark_name)652653base_benchmark_name = benchmark_name.replace('.reference', '')654655if base_benchmark_name not in logdog_dict:656logdog_dict[base_benchmark_name] = {}657658# add links for the perf results and the dashboard url to659# the logs section of buildbot660if is_ref:661if viewer_url:662logdog_dict[base_benchmark_name]['perf_results_ref'] = viewer_url663if upload_failure:664logdog_dict[base_benchmark_name]['ref_upload_failed'] = 'True'665else:666# TODO(jmadill): Figure out if we can get a dashboard URL here. http://anglebug.com/6090667# logdog_dict[base_benchmark_name]['dashboard_url'] = (668# upload_results_to_perf_dashboard.GetDashboardUrl(benchmark_name, configuration_name,669# RESULTS_URL,670# build_properties['got_revision_cp'],671# _GetMachineGroup(build_properties)))672if viewer_url:673logdog_dict[base_benchmark_name]['perf_results'] = viewer_url674if upload_failure:675logdog_dict[base_benchmark_name]['upload_failed'] = 'True'676677678def print_duration(step, start, end):679logging.info('Duration of %s: %d seconds' % (step, end - start))680681682def main():683""" See collect_task.collect_task for more on the merge script API. """684logging.info(sys.argv)685parser = argparse.ArgumentParser()686# configuration-name (previously perf-id) is the name of bot the tests run on687# For example, buildbot-test is the name of the android-go-perf bot688# configuration-name and results-url are set in the json file which is going689# away tools/perf/core/chromium.perf.fyi.extras.json690parser.add_argument('--configuration-name', help=argparse.SUPPRESS)691692parser.add_argument('--build-properties', help=argparse.SUPPRESS)693parser.add_argument('--summary-json', help=argparse.SUPPRESS)694parser.add_argument('--task-output-dir', help=argparse.SUPPRESS)695parser.add_argument('-o', '--output-json', required=True, help=argparse.SUPPRESS)696parser.add_argument(697'--skip-perf',698action='store_true',699help='In lightweight mode, using --skip-perf will skip the performance'700' data handling.')701parser.add_argument(702'--lightweight',703action='store_true',704help='Choose the lightweight mode in which the perf result handling'705' is performed on a separate VM.')706parser.add_argument('json_files', nargs='*', help=argparse.SUPPRESS)707parser.add_argument(708'--smoke-test-mode',709action='store_true',710help='This test should be run in smoke test mode'711' meaning it does not upload to the perf dashboard')712713args = parser.parse_args()714715output_results_dir = tempfile.mkdtemp('outputresults')716try:717return_code, _ = process_perf_results(args.output_json, args.configuration_name,718args.build_properties, args.task_output_dir,719args.smoke_test_mode, output_results_dir,720args.lightweight, args.skip_perf)721return return_code722finally:723shutil.rmtree(output_results_dir)724725726if __name__ == '__main__':727sys.exit(main())728729730