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 23 24def _MergeAPIArgumentParser(*args, **kwargs): 25 """Parameters passed to this merge script, as per: 26 https://chromium.googlesource.com/chromium/tools/build/+/main/scripts/slave/recipe_modules/swarming/resources/merge_api.py 27 """ 28 parser = argparse.ArgumentParser(*args, **kwargs) 29 parser.add_argument('--build-properties', 30 help=argparse.SUPPRESS, 31 default='{}') 32 parser.add_argument('--summary-json', help=argparse.SUPPRESS) 33 parser.add_argument('--task-output-dir', help=argparse.SUPPRESS) 34 parser.add_argument('-o', 35 '--output-json', 36 required=True, 37 help=argparse.SUPPRESS) 38 parser.add_argument('jsons_to_merge', nargs='*', help=argparse.SUPPRESS) 39 40 # Custom arguments for this merge script. 41 parser.add_argument('--additional-merge-script', 42 help='additional merge script to run') 43 parser.add_argument( 44 '--additional-merge-script-args', 45 help='JSON serialized string of args for the additional merge script') 46 parser.add_argument('--profdata-dir', 47 required=True, 48 help='where to store the merged data') 49 parser.add_argument('--llvm-profdata', 50 required=True, 51 help='path to llvm-profdata executable') 52 parser.add_argument('--test-target-name', help='test target name') 53 parser.add_argument('--java-coverage-dir', 54 help='directory for Java coverage data') 55 parser.add_argument('--jacococli-path', help='path to jacococli.jar.') 56 parser.add_argument( 57 '--merged-jacoco-filename', 58 help='filename used to uniquely name the merged exec file.') 59 parser.add_argument('--javascript-coverage-dir', 60 help='directory for JavaScript coverage data') 61 parser.add_argument('--chromium-src-dir', 62 help='directory for chromium/src checkout') 63 parser.add_argument('--build-dir', 64 help='directory for the build directory in chromium/src') 65 parser.add_argument( 66 '--per-cl-coverage', 67 action='store_true', 68 help='set to indicate that this is a per-CL coverage build') 69 parser.add_argument('--sparse', 70 action='store_true', 71 dest='sparse', 72 help='run llvm-profdata with the sparse flag.') 73 # (crbug.com/1091310) - IR PGO is incompatible with the initial conversion 74 # of .profraw -> .profdata that's run to detect validation errors. 75 # Introducing a bypass flag that'll merge all .profraw directly to .profdata 76 parser.add_argument( 77 '--skip-validation', 78 action='store_true', 79 help='skip validation for good raw profile data. this will pass all ' 80 'raw profiles found to llvm-profdata to be merged. only applicable ' 81 'when input extension is .profraw.') 82 return parser 83 84 85def main(): 86 desc = 'Merge profraw files in <--task-output-dir> into a single profdata.' 87 parser = _MergeAPIArgumentParser(description=desc) 88 params = parser.parse_args() 89 90 if params.java_coverage_dir: 91 if not params.jacococli_path: 92 parser.error('--jacococli-path required when merging Java coverage') 93 if not params.merged_jacoco_filename: 94 parser.error( 95 '--merged-jacoco-filename required when merging Java coverage') 96 97 output_path = os.path.join(params.java_coverage_dir, 98 '%s.exec' % params.merged_jacoco_filename) 99 logging.info('Merging JaCoCo .exec files to %s', output_path) 100 profile_merger.merge_java_exec_files(params.task_output_dir, output_path, 101 params.jacococli_path) 102 103 failed = False 104 105 if params.javascript_coverage_dir and params.chromium_src_dir \ 106 and params.build_dir: 107 current_dir = os.path.dirname(__file__) 108 merge_js_results_script = os.path.join(current_dir, 'merge_js_results.py') 109 args = [ 110 sys.executable, 111 merge_js_results_script, 112 '--task-output-dir', 113 params.task_output_dir, 114 '--javascript-coverage-dir', 115 params.javascript_coverage_dir, 116 '--chromium-src-dir', 117 params.chromium_src_dir, 118 '--build-dir', 119 params.build_dir, 120 ] 121 122 rc = subprocess.call(args) 123 if rc != 0: 124 failed = True 125 logging.warning('%s exited with %s', 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), 138 '.profraw', 139 params.llvm_profdata, 140 sparse=params.sparse, 141 skip_validation=params.skip_validation) 142 143 # At the moment counter overflows overlap with invalid profiles, but this is 144 # not guaranteed to remain the case indefinitely. To avoid future conflicts 145 # treat these separately. 146 if counter_overflows: 147 with open(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/40868908): Return non-zero if invalid_profiles is not None 193 return 1 if failed else 0 194 195 196if __name__ == '__main__': 197 logging.basicConfig(format='[%(asctime)s %(levelname)s] %(message)s', 198 level=logging.INFO) 199 sys.exit(main()) 200