Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit de0b58e

Browse files
authored
clang-tidy: added the ability to shard jobs (#37265)
* clang-tidy: added the ability to shard jobs * added test * jenn feedback * hack ci to run as a shard to measure the time * tweak * fix hack * zach feedback * zach feedback 2 * removed stray async * moved to using sets for lookups * fixed typo in docstring * Revert "fix hack" This reverts commit 06a61a6. Revert "tweak" This reverts commit e7c58b1. Revert "hack ci to run as a shard to measure the time" This reverts commit e458963. * removed calls to map * turned the ci hack back on * Revert "turned the ci hack back on" This reverts commit 0d53794. * removed sync*
1 parent e7d7eda commit de0b58e

File tree

3 files changed

+238
-31
lines changed

3 files changed

+238
-31
lines changed

tools/clang_tidy/lib/clang_tidy.dart

Lines changed: 115 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ class _ComputeJobsResult {
2929
final bool sawMalformed;
3030
}
3131

32+
enum _SetStatus {
33+
Intersection,
34+
Difference,
35+
}
36+
37+
class _SetStatusCommand {
38+
_SetStatusCommand(this.setStatus, this.command);
39+
final _SetStatus setStatus;
40+
final Command command;
41+
}
42+
3243
/// A class that runs clang-tidy on all or only the changed files in a git
3344
/// repo.
3445
class ClangTidy {
@@ -92,14 +103,14 @@ class ClangTidy {
92103

93104
_outSink.writeln(_linterOutputHeader);
94105

95-
final List<io.File> changedFiles = await computeChangedFiles();
106+
final List<io.File> filesOfInterest = await computeFilesOfInterest();
96107

97108
if (options.verbose) {
98109
_outSink.writeln('Checking lint in repo at ${options.repoPath.path}.');
99110
if (options.checksArg.isNotEmpty) {
100111
_outSink.writeln('Checking for specific checks: ${options.checks}.');
101112
}
102-
final int changedFilesCount = changedFiles.length;
113+
final int changedFilesCount = filesOfInterest.length;
103114
if (options.lintAll) {
104115
_outSink.writeln('Checking all $changedFilesCount files the repo dir.');
105116
} else {
@@ -109,12 +120,19 @@ class ClangTidy {
109120
}
110121
}
111122

112-
final List<dynamic> buildCommandsData = jsonDecode(
123+
final List<Object?> buildCommandsData = jsonDecode(
113124
options.buildCommandsPath.readAsStringSync(),
114-
) as List<dynamic>;
115-
final List<Command> changedFileBuildCommands = await getLintCommandsForChangedFiles(
125+
) as List<Object?>;
126+
final List<List<Object?>> shardBuildCommandsData = options
127+
.shardCommandsPaths
128+
.map((io.File file) =>
129+
jsonDecode(file.readAsStringSync()) as List<Object?>)
130+
.toList();
131+
final List<Command> changedFileBuildCommands = await getLintCommandsForFiles(
116132
buildCommandsData,
117-
changedFiles,
133+
filesOfInterest,
134+
shardBuildCommandsData,
135+
options.shardId,
118136
);
119137

120138
if (changedFileBuildCommands.isEmpty) {
@@ -153,7 +171,7 @@ class ClangTidy {
153171
/// The files with local modifications or all the files if `lintAll` was
154172
/// specified.
155173
@visibleForTesting
156-
Future<List<io.File>> computeChangedFiles() async {
174+
Future<List<io.File>> computeFilesOfInterest() async {
157175
if (options.lintAll) {
158176
return options.repoPath
159177
.listSync(recursive: true)
@@ -171,23 +189,99 @@ class ClangTidy {
171189
return repo.changedFiles;
172190
}
173191

174-
/// Given a build commands json file, and the files with local changes,
175-
/// compute the lint commands to run.
192+
/// Returns f(n) = value(n * [shardCount] + [id]).
193+
Iterable<T> _takeShard<T>(Iterable<T> values, int id, int shardCount) sync* {
194+
int count = 0;
195+
for (final T val in values) {
196+
if (count % shardCount == id) {
197+
yield val;
198+
}
199+
count++;
200+
}
201+
}
202+
203+
/// This returns a `_SetStatusCommand` for each [Command] in [items].
204+
/// `Intersection` if the Command shows up in [items] and its filePath in all
205+
/// [filePathSets], otherwise `Difference`.
206+
Iterable<_SetStatusCommand> _calcIntersection(
207+
Iterable<Command> items, Iterable<Set<String>> filePathSets) sync* {
208+
bool allSetsContain(Command command) {
209+
for (final Set<String> filePathSet in filePathSets) {
210+
if (!filePathSet.contains(command.filePath)) {
211+
return false;
212+
}
213+
}
214+
return true;
215+
}
216+
for (final Command command in items) {
217+
if (allSetsContain(command)) {
218+
yield _SetStatusCommand(_SetStatus.Intersection, command);
219+
} else {
220+
yield _SetStatusCommand(_SetStatus.Difference, command);
221+
}
222+
}
223+
}
224+
225+
/// Given a build commands json file's contents in [buildCommandsData], and
226+
/// the [files] with local changes, compute the lint commands to run. If
227+
/// build commands are supplied in [sharedBuildCommandsData] the intersection
228+
/// of those build commands will be calculated and distributed across
229+
/// instances via the [shardId].
176230
@visibleForTesting
177-
Future<List<Command>> getLintCommandsForChangedFiles(
178-
List<dynamic> buildCommandsData,
179-
List<io.File> changedFiles,
180-
) async {
181-
final List<Command> buildCommands = <Command>[];
182-
for (final dynamic data in buildCommandsData) {
183-
final Command command = Command.fromMap(data as Map<String, dynamic>);
184-
final LintAction lintAction = await command.lintAction;
185-
// Short-circuit the expensive containsAny call for the many third_party files.
186-
if (lintAction != LintAction.skipThirdParty && command.containsAny(changedFiles)) {
187-
buildCommands.add(command);
231+
Future<List<Command>> getLintCommandsForFiles(
232+
List<Object?> buildCommandsData,
233+
List<io.File> files,
234+
List<List<Object?>> sharedBuildCommandsData,
235+
int? shardId,
236+
) {
237+
final List<Command> totalCommands = <Command>[];
238+
if (sharedBuildCommandsData.isNotEmpty) {
239+
final List<Command> buildCommands = <Command>[
240+
for (Object? data in buildCommandsData)
241+
Command.fromMap((data as Map<String, Object?>?)!)
242+
];
243+
final List<Set<String>> shardFilePaths = <Set<String>>[
244+
for (List<Object?> list in sharedBuildCommandsData)
245+
<String>{
246+
for (Object? data in list)
247+
Command.fromMap((data as Map<String, Object?>?)!).filePath
248+
}
249+
];
250+
final Iterable<_SetStatusCommand> intersectionResults =
251+
_calcIntersection(buildCommands, shardFilePaths);
252+
for (final _SetStatusCommand result in intersectionResults) {
253+
if (result.setStatus == _SetStatus.Difference) {
254+
totalCommands.add(result.command);
255+
}
188256
}
257+
final List<Command> intersection = <Command>[
258+
for (_SetStatusCommand result in intersectionResults)
259+
if (result.setStatus == _SetStatus.Intersection) result.command
260+
];
261+
// Make sure to sort results so the sharding scheme is guaranteed to work
262+
// since we are not sure if there is a defined order in the json file.
263+
intersection
264+
.sort((Command x, Command y) => x.filePath.compareTo(y.filePath));
265+
totalCommands.addAll(
266+
_takeShard(intersection, shardId!, 1 + sharedBuildCommandsData.length));
267+
} else {
268+
totalCommands.addAll(<Command>[
269+
for (Object? data in buildCommandsData)
270+
Command.fromMap((data as Map<String, Object?>?)!)
271+
]);
189272
}
190-
return buildCommands;
273+
return () async {
274+
final List<Command> result = <Command>[];
275+
for (final Command command in totalCommands) {
276+
final LintAction lintAction = await command.lintAction;
277+
// Short-circuit the expensive containsAny call for the many third_party files.
278+
if (lintAction != LintAction.skipThirdParty &&
279+
command.containsAny(files)) {
280+
result.add(command);
281+
}
282+
}
283+
return result;
284+
}();
191285
}
192286

193287
Future<_ComputeJobsResult> _computeJobs(

tools/clang_tidy/lib/src/options.dart

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ class Options {
3434
this.fix = false,
3535
this.errorMessage,
3636
this.warningsAsErrors,
37+
this.shardId,
38+
this.shardCommandsPaths = const <io.File>[],
3739
StringSink? errSink,
3840
}) : checks = checksArg.isNotEmpty ? '--checks=$checksArg' : null,
3941
_errSink = errSink ?? io.stderr;
@@ -64,6 +66,8 @@ class Options {
6466
ArgResults options, {
6567
required io.File buildCommandsPath,
6668
StringSink? errSink,
69+
required List<io.File> shardCommandsPaths,
70+
int? shardId,
6771
}) {
6872
return Options(
6973
help: options['help'] as bool,
@@ -76,6 +80,8 @@ class Options {
7680
fix: options['fix'] as bool,
7781
errSink: errSink,
7882
warningsAsErrors: _platformSpecificWarningsAsErrors(options),
83+
shardCommandsPaths: shardCommandsPaths,
84+
shardId: shardId,
7985
);
8086
}
8187

@@ -87,25 +93,42 @@ class Options {
8793
final ArgResults argResults = _argParser.parse(arguments);
8894

8995
String? buildCommandsPath = argResults['compile-commands'] as String?;
96+
97+
String variantToBuildCommandsFilePath(String variant) =>
98+
path.join(
99+
argResults['src-dir'] as String,
100+
'out',
101+
variant,
102+
'compile_commands.json',
103+
);
90104
// path/to/engine/src/out/variant/compile_commands.json
91-
buildCommandsPath ??= path.join(
92-
argResults['src-dir'] as String,
93-
'out',
94-
argResults['target-variant'] as String,
95-
'compile_commands.json',
96-
);
105+
buildCommandsPath ??= variantToBuildCommandsFilePath(argResults['target-variant'] as String);
97106
final io.File buildCommands = io.File(buildCommandsPath);
107+
final List<io.File> shardCommands =
108+
(argResults['shard-variants'] as String? ?? '')
109+
.split(',')
110+
.where((String element) => element.isNotEmpty)
111+
.map((String variant) =>
112+
io.File(variantToBuildCommandsFilePath(variant)))
113+
.toList();
98114
final String? message = _checkArguments(argResults, buildCommands);
99115
if (message != null) {
100116
return Options._error(message, errSink: errSink);
101117
}
102118
if (argResults['help'] as bool) {
103119
return Options._help(errSink: errSink);
104120
}
121+
final String? shardIdString = argResults['shard-id'] as String?;
122+
final int? shardId = shardIdString == null ? null : int.parse(shardIdString);
123+
if (shardId != null && (shardId >= shardCommands.length || shardId < 0)) {
124+
return Options._error('Invalid shard-id value: $shardId.', errSink: errSink);
125+
}
105126
return Options._fromArgResults(
106127
argResults,
107128
buildCommandsPath: buildCommands,
108129
errSink: errSink,
130+
shardCommandsPaths: shardCommands,
131+
shardId: shardId,
109132
);
110133
}
111134

@@ -132,6 +155,17 @@ class Options {
132155
'verbose',
133156
help: 'Print verbose output.',
134157
)
158+
..addOption(
159+
'shard-id',
160+
help: 'When used with the shard-commands option this identifies which shard will execute.',
161+
valueHelp: 'A number less than 1 + the number of shard-commands arguments.',
162+
)
163+
..addOption(
164+
'shard-variants',
165+
help: 'Comma separated list of other targets, this invocation '
166+
'will only execute a subset of the intersection and the difference of the '
167+
'compile commands. Use with `shard-id`.'
168+
)
135169
..addOption(
136170
'compile-commands',
137171
help: 'Use the given path as the source of compile_commands.json. This '
@@ -171,6 +205,12 @@ class Options {
171205
/// The location of the compile_commands.json file.
172206
final io.File buildCommandsPath;
173207

208+
/// The location of shard compile_commands.json files.
209+
final List<io.File> shardCommandsPaths;
210+
211+
/// The identifier of the shard.
212+
final int? shardId;
213+
174214
/// The root of the flutter/engine repository.
175215
final io.Directory repoPath = io.Directory(_engineRoot);
176216

@@ -233,6 +273,10 @@ class Options {
233273
return "ERROR: Build commands path ${buildCommandsPath.absolute.path} doesn't exist.";
234274
}
235275

276+
if (argResults.wasParsed('shard-variants') && !argResults.wasParsed('shard-id')) {
277+
return 'ERROR: a `shard-id` must be specified with `shard-variants`.';
278+
}
279+
236280
return null;
237281
}
238282
}

0 commit comments

Comments
 (0)