• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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