1#!/usr/bin/env python 2# Copyright 2019 The Chromium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5"""Merge results from code-coverage/pgo swarming runs. 6 7This script merges code-coverage/pgo profiles from multiple shards. It also 8merges the test results of the shards. 9 10It is functionally similar to merge_steps.py but it accepts the parameters 11passed by swarming api. 12""" 13 14import argparse 15import json 16import logging 17import os 18import subprocess 19import sys 20 21import merge_lib as profile_merger 22 23def _MergeAPIArgumentParser(*args, **kwargs): 24 """Parameters passed to this merge script, as per: 25 https://chromium.googlesource.com/chromium/tools/build/+/main/scripts/slave/recipe_modules/swarming/resources/merge_api.py 26 """ 27 parser = argparse.ArgumentParser(*args, **kwargs) 28 parser.add_argument('--build-properties', help=argparse.SUPPRESS, 29 default='{}') 30 parser.add_argument('--summary-json', help=argparse.SUPPRESS) 31 parser.add_argument('--task-output-dir', help=argparse.SUPPRESS) 32 parser.add_argument( 33 '-o', '--output-json', required=True, help=argparse.SUPPRESS) 34 parser.add_argument('jsons_to_merge', nargs='*', help=argparse.SUPPRESS) 35 36 # Custom arguments for this merge script. 37 parser.add_argument( 38 '--additional-merge-script', help='additional merge script to run') 39 parser.add_argument( 40 '--additional-merge-script-args', 41 help='JSON serialized string of args for the additional merge script') 42 parser.add_argument( 43 '--profdata-dir', required=True, help='where to store the merged data') 44 parser.add_argument( 45 '--llvm-profdata', required=True, help='path to llvm-profdata executable') 46 parser.add_argument('--test-target-name', help='test target name') 47 parser.add_argument( 48 '--java-coverage-dir', help='directory for Java coverage data') 49 parser.add_argument( 50 '--jacococli-path', help='path to jacococli.jar.') 51 parser.add_argument( 52 '--merged-jacoco-filename', 53 help='filename used to uniquely name the merged exec file.') 54 parser.add_argument( 55 '--javascript-coverage-dir', 56 help='directory for JavaScript coverage data') 57 parser.add_argument( 58 '--chromium-src-dir', 59 help='directory for chromium/src checkout') 60 parser.add_argument( 61 '--build-dir', 62 help='directory for the build directory in chromium/src') 63 parser.add_argument( 64 '--per-cl-coverage', 65 action='store_true', 66 help='set to indicate that this is a per-CL coverage build') 67 parser.add_argument( 68 '--sparse', 69 action='store_true', 70 dest='sparse', 71 help='run llvm-profdata with the sparse flag.') 72 # (crbug.com/1091310) - IR PGO is incompatible with the initial conversion 73 # of .profraw -> .profdata that's run to detect validation errors. 74 # Introducing a bypass flag that'll merge all .profraw directly to .profdata 75 parser.add_argument( 76 '--skip-validation', 77 action='store_true', 78 help='skip validation for good raw profile data. this will pass all ' 79 'raw profiles found to llvm-profdata to be merged. only applicable ' 80 'when input extension is .profraw.') 81 return parser 82 83 84def main(): 85 desc = "Merge profraw files in <--task-output-dir> into a single profdata." 86 parser = _MergeAPIArgumentParser(description=desc) 87 params = parser.parse_args() 88 89 if params.java_coverage_dir: 90 if not params.jacococli_path: 91 parser.error('--jacococli-path required when merging Java coverage') 92 if not params.merged_jacoco_filename: 93 parser.error( 94 '--merged-jacoco-filename required when merging Java coverage') 95 96 output_path = os.path.join( 97 params.java_coverage_dir, '%s.exec' % params.merged_jacoco_filename) 98 logging.info('Merging JaCoCo .exec files to %s', output_path) 99 profile_merger.merge_java_exec_files( 100 params.task_output_dir, output_path, params.jacococli_path) 101 102 failed = False 103 104 if params.javascript_coverage_dir and params.chromium_src_dir \ 105 and params.build_dir: 106 current_dir = os.path.dirname(__file__) 107 merge_js_results_script = os.path.join(current_dir, 'merge_js_results.py') 108 args = [ 109 sys.executable, 110 merge_js_results_script, 111 '--task-output-dir', 112 params.task_output_dir, 113 '--javascript-coverage-dir', 114 params.javascript_coverage_dir, 115 '--chromium-src-dir', 116 params.chromium_src_dir, 117 '--build-dir', 118 params.build_dir, 119 ] 120 121 rc = subprocess.call(args) 122 if rc != 0: 123 failed = True 124 logging.warning('%s exited with %s' % 125 (merge_js_results_script, rc)) 126 127 # Name the output profdata file name as {test_target}.profdata or 128 # default.profdata. 129 output_prodata_filename = (params.test_target_name or 'default') + '.profdata' 130 131 # NOTE: The profile data merge script must make sure that the profraw files 132 # are deleted from the task output directory after merging, otherwise, other 133 # test results merge script such as layout tests will treat them as json test 134 # results files and result in errors. 135 invalid_profiles, counter_overflows = profile_merger.merge_profiles( 136 params.task_output_dir, 137 os.path.join(params.profdata_dir, output_prodata_filename), '.profraw', 138 params.llvm_profdata, 139 sparse=params.sparse, 140 skip_validation=params.skip_validation) 141 142 # At the moment counter overflows overlap with invalid profiles, but this is 143 # not guaranteed to remain the case indefinitely. To avoid future conflicts 144 # treat these separately. 145 if counter_overflows: 146 with open( 147 os.path.join(params.profdata_dir, 'profiles_with_overflows.json'), 148 'w') as f: 149 json.dump(counter_overflows, f) 150 151 if invalid_profiles: 152 with open(os.path.join(params.profdata_dir, 'invalid_profiles.json'), 153 'w') as f: 154 json.dump(invalid_profiles, f) 155 156 # If given, always run the additional merge script, even if we only have one 157 # output json. Merge scripts sometimes upload artifacts to cloud storage, or 158 # do other processing which can be needed even if there's only one output. 159 if params.additional_merge_script: 160 new_args = [ 161 '--build-properties', 162 params.build_properties, 163 '--summary-json', 164 params.summary_json, 165 '--task-output-dir', 166 params.task_output_dir, 167 '--output-json', 168 params.output_json, 169 ] 170 171 if params.additional_merge_script_args: 172 new_args += json.loads(params.additional_merge_script_args) 173 174 new_args += params.jsons_to_merge 175 176 args = [sys.executable, params.additional_merge_script] + new_args 177 rc = subprocess.call(args) 178 if rc != 0: 179 failed = True 180 logging.warning('Additional merge script %s exited with %s' % 181 (params.additional_merge_script, rc)) 182 elif len(params.jsons_to_merge) == 1: 183 logging.info("Only one output needs to be merged; directly copying it.") 184 with open(params.jsons_to_merge[0]) as f_read: 185 with open(params.output_json, 'w') as f_write: 186 f_write.write(f_read.read()) 187 else: 188 logging.warning( 189 'This script was told to merge test results, but no additional merge ' 190 'script was given.') 191 192 # TODO(crbug.com/1369158): Return non-zero if invalid_profiles is not None 193 return 1 if failed else 0 194 195 196if __name__ == '__main__': 197 logging.basicConfig( 198 format='[%(asctime)s %(levelname)s] %(message)s', level=logging.INFO) 199 sys.exit(main()) 200