| #!/usr/bin/env python |
| # Copyright 2019 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Merge results from code-coverage/pgo swarming runs. |
| |
| This script merges code-coverage/pgo profiles from multiple shards. It also |
| merges the test results of the shards. |
| |
| It is functionally similar to merge_steps.py but it accepts the parameters |
| passed by swarming api. |
| """ |
| |
| import argparse |
| import json |
| import logging |
| import os |
| import subprocess |
| import sys |
| |
| import merge_lib as profile_merger |
| |
| |
| def _MergeAPIArgumentParser(*args, **kwargs): |
| """Parameters passed to this merge script, as per: |
| https://chromium.googlesource.com/chromium/tools/build/+/main/scripts/slave/recipe_modules/swarming/resources/merge_api.py |
| """ |
| parser = argparse.ArgumentParser(*args, **kwargs) |
| parser.add_argument('--build-properties', |
| help=argparse.SUPPRESS, |
| default='{}') |
| parser.add_argument('--summary-json', help=argparse.SUPPRESS) |
| parser.add_argument('--task-output-dir', help=argparse.SUPPRESS) |
| parser.add_argument('-o', |
| '--output-json', |
| required=True, |
| help=argparse.SUPPRESS) |
| parser.add_argument('jsons_to_merge', nargs='*', help=argparse.SUPPRESS) |
| |
| # Custom arguments for this merge script. |
| parser.add_argument('--additional-merge-script', |
| help='additional merge script to run') |
| parser.add_argument( |
| '--additional-merge-script-args', |
| help='JSON serialized string of args for the additional merge script') |
| parser.add_argument('--profdata-dir', |
| required=True, |
| help='where to store the merged data') |
| parser.add_argument('--llvm-profdata', |
| required=True, |
| help='path to llvm-profdata executable') |
| parser.add_argument('--test-target-name', help='test target name') |
| parser.add_argument('--java-coverage-dir', |
| help='directory for Java coverage data') |
| parser.add_argument('--jacococli-path', help='path to jacococli.jar.') |
| parser.add_argument( |
| '--merged-jacoco-filename', |
| help='filename used to uniquely name the merged exec file.') |
| parser.add_argument('--javascript-coverage-dir', |
| help='directory for JavaScript coverage data') |
| parser.add_argument('--chromium-src-dir', |
| help='directory for chromium/src checkout') |
| parser.add_argument('--build-dir', |
| help='directory for the build directory in chromium/src') |
| parser.add_argument( |
| '--per-cl-coverage', |
| action='store_true', |
| help='set to indicate that this is a per-CL coverage build') |
| parser.add_argument('--sparse', |
| action='store_true', |
| dest='sparse', |
| help='run llvm-profdata with the sparse flag.') |
| # (crbug.com/1091310) - IR PGO is incompatible with the initial conversion |
| # of .profraw -> .profdata that's run to detect validation errors. |
| # Introducing a bypass flag that'll merge all .profraw directly to .profdata |
| parser.add_argument( |
| '--skip-validation', |
| action='store_true', |
| help='skip validation for good raw profile data. this will pass all ' |
| 'raw profiles found to llvm-profdata to be merged. only applicable ' |
| 'when input extension is .profraw.') |
| return parser |
| |
| |
| def main(): |
| desc = 'Merge profraw files in <--task-output-dir> into a single profdata.' |
| parser = _MergeAPIArgumentParser(description=desc) |
| params = parser.parse_args() |
| |
| if params.java_coverage_dir: |
| if not params.jacococli_path: |
| parser.error('--jacococli-path required when merging Java coverage') |
| if not params.merged_jacoco_filename: |
| parser.error( |
| '--merged-jacoco-filename required when merging Java coverage') |
| |
| output_path = os.path.join(params.java_coverage_dir, |
| '%s.exec' % params.merged_jacoco_filename) |
| logging.info('Merging JaCoCo .exec files to %s', output_path) |
| profile_merger.merge_java_exec_files(params.task_output_dir, output_path, |
| params.jacococli_path) |
| |
| failed = False |
| |
| if params.javascript_coverage_dir and params.chromium_src_dir \ |
| and params.build_dir: |
| current_dir = os.path.dirname(__file__) |
| merge_js_results_script = os.path.join(current_dir, 'merge_js_results.py') |
| args = [ |
| sys.executable, |
| merge_js_results_script, |
| '--task-output-dir', |
| params.task_output_dir, |
| '--javascript-coverage-dir', |
| params.javascript_coverage_dir, |
| '--chromium-src-dir', |
| params.chromium_src_dir, |
| '--build-dir', |
| params.build_dir, |
| ] |
| |
| rc = subprocess.call(args) |
| if rc != 0: |
| failed = True |
| logging.warning('%s exited with %s', merge_js_results_script, rc) |
| |
| # Name the output profdata file name as {test_target}.profdata or |
| # default.profdata. |
| output_prodata_filename = (params.test_target_name or 'default') + '.profdata' |
| |
| # NOTE: The profile data merge script must make sure that the profraw files |
| # are deleted from the task output directory after merging, otherwise, other |
| # test results merge script such as layout tests will treat them as json test |
| # results files and result in errors. |
| invalid_profiles, counter_overflows = profile_merger.merge_profiles( |
| params.task_output_dir, |
| os.path.join(params.profdata_dir, output_prodata_filename), |
| '.profraw', |
| params.llvm_profdata, |
| sparse=params.sparse, |
| skip_validation=params.skip_validation) |
| |
| # At the moment counter overflows overlap with invalid profiles, but this is |
| # not guaranteed to remain the case indefinitely. To avoid future conflicts |
| # treat these separately. |
| if counter_overflows: |
| with open(os.path.join(params.profdata_dir, 'profiles_with_overflows.json'), |
| 'w') as f: |
| json.dump(counter_overflows, f) |
| |
| if invalid_profiles: |
| with open(os.path.join(params.profdata_dir, 'invalid_profiles.json'), |
| 'w') as f: |
| json.dump(invalid_profiles, f) |
| |
| # If given, always run the additional merge script, even if we only have one |
| # output json. Merge scripts sometimes upload artifacts to cloud storage, or |
| # do other processing which can be needed even if there's only one output. |
| if params.additional_merge_script: |
| new_args = [ |
| '--build-properties', |
| params.build_properties, |
| '--summary-json', |
| params.summary_json, |
| '--task-output-dir', |
| params.task_output_dir, |
| '--output-json', |
| params.output_json, |
| ] |
| |
| if params.additional_merge_script_args: |
| new_args += json.loads(params.additional_merge_script_args) |
| |
| new_args += params.jsons_to_merge |
| |
| args = [sys.executable, params.additional_merge_script] + new_args |
| rc = subprocess.call(args) |
| if rc != 0: |
| failed = True |
| logging.warning('Additional merge script %s exited with %s', |
| params.additional_merge_script, rc) |
| elif len(params.jsons_to_merge) == 1: |
| logging.info('Only one output needs to be merged; directly copying it.') |
| with open(params.jsons_to_merge[0]) as f_read: |
| with open(params.output_json, 'w') as f_write: |
| f_write.write(f_read.read()) |
| else: |
| logging.warning( |
| 'This script was told to merge test results, but no additional merge ' |
| 'script was given.') |
| |
| # TODO(crbug.com/40868908): Return non-zero if invalid_profiles is not None |
| return 1 if failed else 0 |
| |
| |
| if __name__ == '__main__': |
| logging.basicConfig(format='[%(asctime)s %(levelname)s] %(message)s', |
| level=logging.INFO) |
| sys.exit(main()) |