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

Commit be68606

Browse files
committed
clang-tidy: added the ability to shard jobs
1 parent cdfd9d0 commit be68606

File tree

3 files changed

+149
-25
lines changed

3 files changed

+149
-25
lines changed

tools/clang_tidy/lib/clang_tidy.dart

Lines changed: 91 additions & 15 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 {
@@ -112,9 +123,16 @@ class ClangTidy {
112123
final List<dynamic> buildCommandsData = jsonDecode(
113124
options.buildCommandsPath.readAsStringSync(),
114125
) as List<dynamic>;
115-
final List<Command> changedFileBuildCommands = await getLintCommandsForChangedFiles(
126+
final List<List<dynamic>> shardBuildCommandsData = options
127+
.shardCommandsPaths
128+
.map((io.File file) =>
129+
jsonDecode(file.readAsStringSync()) as List<dynamic>)
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,81 @@ class ClangTidy {
171189
return repo.changedFiles;
172190
}
173191

192+
Iterable<T> _takeShard<T>(Iterable<T> values, int id, int shardCount) sync*{
193+
int count = 0;
194+
for(final T val in values) {
195+
if (count % shardCount == id) {
196+
yield val;
197+
}
198+
count++;
199+
}
200+
}
201+
202+
Iterable<_SetStatusCommand> _calcIntersection(Iterable<Command> items, Iterable<List<Command>> sets) sync* {
203+
bool allSetsContain(Command command) {
204+
for (final List<Command> set in sets) {
205+
final Iterable<String> filePaths = set.map((Command e) => e.filePath);
206+
if (!filePaths.contains(command.filePath)) {
207+
return false;
208+
}
209+
}
210+
return true;
211+
}
212+
for (final Command command in items) {
213+
if (allSetsContain(command)) {
214+
yield _SetStatusCommand(_SetStatus.Intersection, command);
215+
} else {
216+
yield _SetStatusCommand(_SetStatus.Difference, command);
217+
}
218+
}
219+
}
220+
174221
/// Given a build commands json file, and the files with local changes,
175222
/// compute the lint commands to run.
176223
@visibleForTesting
177-
Future<List<Command>> getLintCommandsForChangedFiles(
224+
Future<List<Command>> getLintCommandsForFiles(
178225
List<dynamic> buildCommandsData,
179-
List<io.File> changedFiles,
226+
List<io.File> files,
227+
List<List<dynamic>> sharedBuildCommandsData,
228+
int? shardId,
180229
) 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);
230+
final List<Command> totalCommands = <Command>[];
231+
if (sharedBuildCommandsData.isNotEmpty) {
232+
final Iterable<Command> buildCommands = buildCommandsData
233+
.map((dynamic data) => Command.fromMap(data as Map<String, dynamic>));
234+
final Iterable<List<Command>> shardBuildCommands =
235+
sharedBuildCommandsData.map((List<dynamic> list) => list
236+
.map((dynamic data) =>
237+
Command.fromMap(data as Map<String, dynamic>))
238+
.toList());
239+
final Iterable<_SetStatusCommand> intersectionResults =
240+
_calcIntersection(buildCommands, shardBuildCommands);
241+
totalCommands.addAll(intersectionResults
242+
.where((_SetStatusCommand element) =>
243+
element.setStatus == _SetStatus.Difference)
244+
.map((_SetStatusCommand e) => e.command));
245+
final List<Command> intersection = intersectionResults
246+
.where((_SetStatusCommand element) =>
247+
element.setStatus == _SetStatus.Intersection)
248+
.map((_SetStatusCommand e) => e.command)
249+
.toList();
250+
// Make sure to sort results, not sure if there is a defined order in the json file.
251+
intersection.sort((Command x, Command y) => x.filePath.compareTo(y.filePath));
252+
totalCommands.addAll(
253+
_takeShard(intersection, shardId!, 1 + shardBuildCommands.length));
254+
} else {
255+
totalCommands.addAll(buildCommandsData.map((dynamic data) => Command.fromMap(data as Map<String, dynamic>)));
256+
}
257+
Stream<Command> filterCommands() async* {
258+
for (final Command command in totalCommands) {
259+
final LintAction lintAction = await command.lintAction;
260+
// Short-circuit the expensive containsAny call for the many third_party files.
261+
if (lintAction != LintAction.skipThirdParty && command.containsAny(files)) {
262+
yield command;
263+
}
188264
}
189265
}
190-
return buildCommands;
266+
return filterCommands().toList();
191267
}
192268

193269
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
}

tools/clang_tidy/test/clang_tidy_test.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ Future<int> main(List<String> args) async {
193193
outSink: outBuffer,
194194
errSink: errBuffer,
195195
);
196-
final List<io.File> fileList = await clangTidy.computeChangedFiles();
196+
final List<io.File> fileList = await clangTidy.computeFilesOfInterest();
197197
expect(fileList.length, greaterThan(1000));
198198
});
199199

@@ -205,7 +205,7 @@ Future<int> main(List<String> args) async {
205205
outSink: outBuffer,
206206
errSink: errBuffer,
207207
);
208-
final List<io.File> fileList = await clangTidy.computeChangedFiles();
208+
final List<io.File> fileList = await clangTidy.computeFilesOfInterest();
209209
expect(fileList.length, lessThan(300));
210210
});
211211

@@ -226,9 +226,11 @@ Future<int> main(List<String> args) async {
226226
'file': filePath,
227227
},
228228
];
229-
final List<Command> commands = await clangTidy.getLintCommandsForChangedFiles(
229+
final List<Command> commands = await clangTidy.getLintCommandsForFiles(
230230
buildCommandsData,
231231
<io.File>[],
232+
<List<dynamic>>[],
233+
null,
232234
);
233235

234236
expect(commands, isEmpty);
@@ -253,9 +255,11 @@ Future<int> main(List<String> args) async {
253255
'file': filePath,
254256
},
255257
];
256-
final List<Command> commands = await clangTidy.getLintCommandsForChangedFiles(
258+
final List<Command> commands = await clangTidy.getLintCommandsForFiles(
257259
buildCommandsData,
258260
<io.File>[io.File(filePath)],
261+
<List<dynamic>>[],
262+
null,
259263
);
260264

261265
expect(commands, isNotEmpty);

0 commit comments

Comments
 (0)