• 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
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