-
Notifications
You must be signed in to change notification settings - Fork 6k
clang-tidy: added the ability to shard jobs #37265
Changes from all commits
20f3910
ee8469e
56ab558
e458963
e7c58b1
06a61a6
e8527b3
664deb7
62a6607
d968147
fa65733
a1438ab
72c6a87
0d53794
e33c6a1
bb2ded4
b68d266
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,17 @@ class _ComputeJobsResult { | |
final bool sawMalformed; | ||
} | ||
|
||
enum _SetStatus { | ||
Intersection, | ||
Difference, | ||
} | ||
|
||
class _SetStatusCommand { | ||
_SetStatusCommand(this.setStatus, this.command); | ||
final _SetStatus setStatus; | ||
final Command command; | ||
} | ||
|
||
/// A class that runs clang-tidy on all or only the changed files in a git | ||
/// repo. | ||
class ClangTidy { | ||
|
@@ -92,14 +103,14 @@ class ClangTidy { | |
|
||
_outSink.writeln(_linterOutputHeader); | ||
|
||
final List<io.File> changedFiles = await computeChangedFiles(); | ||
final List<io.File> filesOfInterest = await computeFilesOfInterest(); | ||
|
||
if (options.verbose) { | ||
_outSink.writeln('Checking lint in repo at ${options.repoPath.path}.'); | ||
if (options.checksArg.isNotEmpty) { | ||
_outSink.writeln('Checking for specific checks: ${options.checks}.'); | ||
} | ||
final int changedFilesCount = changedFiles.length; | ||
final int changedFilesCount = filesOfInterest.length; | ||
if (options.lintAll) { | ||
_outSink.writeln('Checking all $changedFilesCount files the repo dir.'); | ||
} else { | ||
|
@@ -109,12 +120,19 @@ class ClangTidy { | |
} | ||
} | ||
|
||
final List<dynamic> buildCommandsData = jsonDecode( | ||
final List<Object?> buildCommandsData = jsonDecode( | ||
options.buildCommandsPath.readAsStringSync(), | ||
) as List<dynamic>; | ||
final List<Command> changedFileBuildCommands = await getLintCommandsForChangedFiles( | ||
) as List<Object?>; | ||
final List<List<Object?>> shardBuildCommandsData = options | ||
.shardCommandsPaths | ||
.map((io.File file) => | ||
jsonDecode(file.readAsStringSync()) as List<Object?>) | ||
.toList(); | ||
final List<Command> changedFileBuildCommands = await getLintCommandsForFiles( | ||
buildCommandsData, | ||
changedFiles, | ||
filesOfInterest, | ||
shardBuildCommandsData, | ||
options.shardId, | ||
); | ||
|
||
if (changedFileBuildCommands.isEmpty) { | ||
|
@@ -153,7 +171,7 @@ class ClangTidy { | |
/// The files with local modifications or all the files if `lintAll` was | ||
/// specified. | ||
@visibleForTesting | ||
Future<List<io.File>> computeChangedFiles() async { | ||
Future<List<io.File>> computeFilesOfInterest() async { | ||
if (options.lintAll) { | ||
return options.repoPath | ||
.listSync(recursive: true) | ||
|
@@ -171,23 +189,99 @@ class ClangTidy { | |
return repo.changedFiles; | ||
} | ||
|
||
/// Given a build commands json file, and the files with local changes, | ||
/// compute the lint commands to run. | ||
/// Returns f(n) = value(n * [shardCount] + [id]). | ||
Iterable<T> _takeShard<T>(Iterable<T> values, int id, int shardCount) sync* { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the code from here down that is reasoning about sets of commands could be made a bit more idiomatic, and maybe more readable as well, by encapsulating it in a new class ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Respectfully, I don't think that will help make the code easier to read or maintain. The small codebase already has poor oop design already and it actually made my job harder. In objective terms adding more classes will result in more lines of code, more segmentation of the code, and more concepts you need to know to modify the code. As an example, there is no reason for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The code in
These were just naming suggestions for a single new class. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea, I think the documentation could be better and will help with that. I've updated the docstring in |
||
int count = 0; | ||
for (final T val in values) { | ||
if (count % shardCount == id) { | ||
yield val; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe instead of the sync* function: return Iterable.generate(
values.length / shardCount,
(int index) => values.elementAt(index * shardCount + id),
); but without the likely off-by-one errors. |
||
} | ||
count++; | ||
} | ||
} | ||
|
||
/// This returns a `_SetStatusCommand` for each [Command] in [items]. | ||
/// `Intersection` if the Command shows up in [items] and its filePath in all | ||
/// [filePathSets], otherwise `Difference`. | ||
Iterable<_SetStatusCommand> _calcIntersection( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The following is more idiomatic. You can use Iterable<_SetStatusCommand> _calcIntersection(
Iterable<Commands> items,
Iterable<Set> filePathSets,
) {
bool allSetsContain(Command command) {
return filePathSets.every(
(Set<String> filePathSet) => filePathSet.contains(command.filePath),
);
}
return <_SetStatusCommand>[
for (final Command command in items)
if (allSetsContain(command))
_SetStatusCommand(_SetStatus.Intersection, command)
else
_SetStatusCommand(_SetStatus.Difference, command),
];
} |
||
Iterable<Command> items, Iterable<Set<String>> filePathSets) sync* { | ||
bool allSetsContain(Command command) { | ||
for (final Set<String> filePathSet in filePathSets) { | ||
if (!filePathSet.contains(command.filePath)) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
for (final Command command in items) { | ||
if (allSetsContain(command)) { | ||
yield _SetStatusCommand(_SetStatus.Intersection, command); | ||
} else { | ||
yield _SetStatusCommand(_SetStatus.Difference, command); | ||
} | ||
} | ||
} | ||
|
||
/// Given a build commands json file's contents in [buildCommandsData], and | ||
/// the [files] with local changes, compute the lint commands to run. If | ||
/// build commands are supplied in [sharedBuildCommandsData] the intersection | ||
/// of those build commands will be calculated and distributed across | ||
/// instances via the [shardId]. | ||
@visibleForTesting | ||
Future<List<Command>> getLintCommandsForChangedFiles( | ||
List<dynamic> buildCommandsData, | ||
List<io.File> changedFiles, | ||
) async { | ||
final List<Command> buildCommands = <Command>[]; | ||
for (final dynamic data in buildCommandsData) { | ||
final Command command = Command.fromMap(data as Map<String, dynamic>); | ||
final LintAction lintAction = await command.lintAction; | ||
// Short-circuit the expensive containsAny call for the many third_party files. | ||
if (lintAction != LintAction.skipThirdParty && command.containsAny(changedFiles)) { | ||
buildCommands.add(command); | ||
Future<List<Command>> getLintCommandsForFiles( | ||
List<Object?> buildCommandsData, | ||
List<io.File> files, | ||
List<List<Object?>> sharedBuildCommandsData, | ||
int? shardId, | ||
) { | ||
final List<Command> totalCommands = <Command>[]; | ||
if (sharedBuildCommandsData.isNotEmpty) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re-writing with collection-for I think is an improvement. if (sharedBuildCommandsData.isNotEmpty) {
final Iterable<Command> buildCommands = <Command>[
for (Object? data in buildCommandsData)
Command.fromMap(data as Map<String, Object?>),
];
final Iterable<Set<String>> shardFilePaths = <Set<String>>[
for (List<Object?> list in sharedBuildCommandsData) {
for (Object? data in list)
Command.fromMap((data as Map<String, Object?>?)!).filePath,
},
];
final Iterable<_SetStatusCommand> intersectionResults = _calcIntersection(
buildCommands, shardFilePaths,
);
totalCommands.addAll(<Command>[
for (_SetStatusCommand element in intersectionResults)
if (element.setStatus == _SetStatus.Difference)
element.command,
]);
final List<Command> intersection = <Command>[
for (_SetStatusCommand element in intersectionResults)
if (element.setStatus == _SetStatus.Intersection)
element.command,
];
totalCommands.addAll(_takeShard(
intersection,
shardId!,
1 + sharedBuildCommandsData.length,
));
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
final List<Command> buildCommands = <Command>[ | ||
for (Object? data in buildCommandsData) | ||
Command.fromMap((data as Map<String, Object?>?)!) | ||
]; | ||
final List<Set<String>> shardFilePaths = <Set<String>>[ | ||
for (List<Object?> list in sharedBuildCommandsData) | ||
<String>{ | ||
for (Object? data in list) | ||
Command.fromMap((data as Map<String, Object?>?)!).filePath | ||
} | ||
]; | ||
final Iterable<_SetStatusCommand> intersectionResults = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method could use some comments. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
_calcIntersection(buildCommands, shardFilePaths); | ||
for (final _SetStatusCommand result in intersectionResults) { | ||
if (result.setStatus == _SetStatus.Difference) { | ||
totalCommands.add(result.command); | ||
} | ||
} | ||
final List<Command> intersection = <Command>[ | ||
for (_SetStatusCommand result in intersectionResults) | ||
if (result.setStatus == _SetStatus.Intersection) result.command | ||
]; | ||
// Make sure to sort results so the sharding scheme is guaranteed to work | ||
// since we are not sure if there is a defined order in the json file. | ||
intersection | ||
.sort((Command x, Command y) => x.filePath.compareTo(y.filePath)); | ||
totalCommands.addAll( | ||
_takeShard(intersection, shardId!, 1 + sharedBuildCommandsData.length)); | ||
} else { | ||
totalCommands.addAll(<Command>[ | ||
for (Object? data in buildCommandsData) | ||
Command.fromMap((data as Map<String, Object?>?)!) | ||
]); | ||
} | ||
return buildCommands; | ||
return () async { | ||
final List<Command> result = <Command>[]; | ||
for (final Command command in totalCommands) { | ||
final LintAction lintAction = await command.lintAction; | ||
// Short-circuit the expensive containsAny call for the many third_party files. | ||
if (lintAction != LintAction.skipThirdParty && | ||
command.containsAny(files)) { | ||
result.add(command); | ||
} | ||
} | ||
return result; | ||
}(); | ||
} | ||
|
||
Future<_ComputeJobsResult> _computeJobs( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,8 @@ class Options { | |
this.fix = false, | ||
this.errorMessage, | ||
this.warningsAsErrors, | ||
this.shardId, | ||
this.shardCommandsPaths = const <io.File>[], | ||
StringSink? errSink, | ||
}) : checks = checksArg.isNotEmpty ? '--checks=$checksArg' : null, | ||
_errSink = errSink ?? io.stderr; | ||
|
@@ -64,6 +66,8 @@ class Options { | |
ArgResults options, { | ||
required io.File buildCommandsPath, | ||
StringSink? errSink, | ||
required List<io.File> shardCommandsPaths, | ||
int? shardId, | ||
}) { | ||
return Options( | ||
help: options['help'] as bool, | ||
|
@@ -76,6 +80,8 @@ class Options { | |
fix: options['fix'] as bool, | ||
errSink: errSink, | ||
warningsAsErrors: _platformSpecificWarningsAsErrors(options), | ||
shardCommandsPaths: shardCommandsPaths, | ||
shardId: shardId, | ||
); | ||
} | ||
|
||
|
@@ -87,25 +93,42 @@ class Options { | |
final ArgResults argResults = _argParser.parse(arguments); | ||
|
||
String? buildCommandsPath = argResults['compile-commands'] as String?; | ||
|
||
String variantToBuildCommandsFilePath(String variant) => | ||
path.join( | ||
argResults['src-dir'] as String, | ||
'out', | ||
variant, | ||
'compile_commands.json', | ||
); | ||
// path/to/engine/src/out/variant/compile_commands.json | ||
buildCommandsPath ??= path.join( | ||
argResults['src-dir'] as String, | ||
'out', | ||
argResults['target-variant'] as String, | ||
'compile_commands.json', | ||
); | ||
buildCommandsPath ??= variantToBuildCommandsFilePath(argResults['target-variant'] as String); | ||
final io.File buildCommands = io.File(buildCommandsPath); | ||
final List<io.File> shardCommands = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. final String shardVariants = argResults['shard-variants'] as String? ?? '';
final List<io.File> shardCommands = <io.File>[
for (String element in buildCommands.split(','))
if (element.isNotEmpty)
io.FIle(variantToBuildCommandsFilePath(element)),
]; |
||
(argResults['shard-variants'] as String? ?? '') | ||
.split(',') | ||
.where((String element) => element.isNotEmpty) | ||
.map((String variant) => | ||
io.File(variantToBuildCommandsFilePath(variant))) | ||
.toList(); | ||
final String? message = _checkArguments(argResults, buildCommands); | ||
if (message != null) { | ||
return Options._error(message, errSink: errSink); | ||
} | ||
if (argResults['help'] as bool) { | ||
return Options._help(errSink: errSink); | ||
} | ||
final String? shardIdString = argResults['shard-id'] as String?; | ||
final int? shardId = shardIdString == null ? null : int.parse(shardIdString); | ||
if (shardId != null && (shardId >= shardCommands.length || shardId < 0)) { | ||
return Options._error('Invalid shard-id value: $shardId.', errSink: errSink); | ||
} | ||
return Options._fromArgResults( | ||
argResults, | ||
buildCommandsPath: buildCommands, | ||
errSink: errSink, | ||
shardCommandsPaths: shardCommands, | ||
shardId: shardId, | ||
); | ||
} | ||
|
||
|
@@ -132,6 +155,17 @@ class Options { | |
'verbose', | ||
help: 'Print verbose output.', | ||
) | ||
..addOption( | ||
'shard-id', | ||
help: 'When used with the shard-commands option this identifies which shard will execute.', | ||
valueHelp: 'A number less than 1 + the number of shard-commands arguments.', | ||
) | ||
..addOption( | ||
'shard-variants', | ||
help: 'Comma separated list of other targets, this invocation ' | ||
'will only execute a subset of the intersection and the difference of the ' | ||
'compile commands. Use with `shard-id`.' | ||
) | ||
..addOption( | ||
'compile-commands', | ||
help: 'Use the given path as the source of compile_commands.json. This ' | ||
|
@@ -171,6 +205,12 @@ class Options { | |
/// The location of the compile_commands.json file. | ||
final io.File buildCommandsPath; | ||
|
||
/// The location of shard compile_commands.json files. | ||
final List<io.File> shardCommandsPaths; | ||
|
||
/// The identifier of the shard. | ||
final int? shardId; | ||
|
||
/// The root of the flutter/engine repository. | ||
final io.Directory repoPath = io.Directory(_engineRoot); | ||
|
||
|
@@ -233,6 +273,10 @@ class Options { | |
return "ERROR: Build commands path ${buildCommandsPath.absolute.path} doesn't exist."; | ||
} | ||
|
||
if (argResults.wasParsed('shard-variants') && !argResults.wasParsed('shard-id')) { | ||
return 'ERROR: a `shard-id` must be specified with `shard-variants`.'; | ||
} | ||
|
||
return null; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"collection-for" is more idiomatic: