#!/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())