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