-
Notifications
You must be signed in to change notification settings - Fork 6k
clang-tidy: added the ability to shard jobs #37265
Changes from 2 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 { | ||||||
|
|
@@ -112,9 +123,16 @@ class ClangTidy { | |||||
| final List<dynamic> buildCommandsData = jsonDecode( | ||||||
| options.buildCommandsPath.readAsStringSync(), | ||||||
| ) as List<dynamic>; | ||||||
| final List<Command> changedFileBuildCommands = await getLintCommandsForChangedFiles( | ||||||
| final List<List<dynamic>> shardBuildCommandsData = options | ||||||
| .shardCommandsPaths | ||||||
| .map((io.File file) => | ||||||
| jsonDecode(file.readAsStringSync()) as List<dynamic>) | ||||||
| .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,82 @@ class ClangTidy { | |||||
| return repo.changedFiles; | ||||||
| } | ||||||
|
|
||||||
| Iterable<T> _takeShard<T>(Iterable<T> values, int id, int shardCount) sync*{ | ||||||
|
||||||
| Iterable<T> _takeShard<T>(Iterable<T> values, int id, int shardCount) sync*{ | |
| 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 comment
The reason will be displayed to describe this comment to others. Learn more.
done
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.
As with the framework repo, dartfmt isn't run in the engine repo.
Outdated
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.
| for(final T val in values) { | |
| for (final T val in values) { |
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.
done
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.
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.
Outdated
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.
Would the set math be easier making the parameter literally a set Iterable<Set<Command>>?
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.
Like can we get rid of some of this code by taking advantage of Set? I don't think so. We'd have to add hashing ability to Command and make clear what it means to look up a Command by file path. So in that sense it will make the code more complicated. It might make things a bit faster however.
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.
Done, moved to Set<String> to speed up intersection calculation.
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
done
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.
This method could use some comments.
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.
done
Outdated
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.
I suspect the analysis order doesn't matter. Have you tried without this?
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.
It matters because it's how the sharding scheme works. We have to guarantee that all shards are starting with the same order to make sure that their jobs don't overlap.
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.
Okay, I think I'm missing something big. Could you explain a bit more about why the order matters, or what would go wrong if the jobs are out of order?
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.
Yep, so imagine we are sharding across iOS and mac. They both generated their compile_commands.json files. They look like this:
iOS
[
{ "path": "foo.cc" },
{ "path": "bar.cc" },
]
mac
[
{ "path": "bar.cc" },
{ "path": "foo.cc" },
]
If you ran mac as shard 0 and ios and shard 1 (where job(n) = n * 2 + shard), they will both just execute on "foo.cc". We don't have a guarantee about the order of compile_command.json as far as I know, so they have to be sorted.
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.
Got it. This explanation should probably go in a comment in the code.
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.
expanded on the existing comment
Outdated
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.
Should shardId param be nullable if it's going to get ! here?
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.
The value isn't used when sharedBuildCommandsData.isEmpty (ie when sharding isn't used).
Outdated
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.
Please don't add new uses of Streams / async*.
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.
Are you referring to the guidelines for editing code in the framework? That doesn't apply here, right?
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.
Dart conventions from the framework repo do apply here, yes. But independent of that, Streams and async* make code very difficult to debug, so should be avoided in general.
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.
Done. I think the framework repo recommendation lists alternatives to streams that don't exist here. Nevertheless, I removed the Stream and the async*.
| 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 = | ||
|
Member
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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -193,7 +193,7 @@ Future<int> main(List<String> args) async { | |
| outSink: outBuffer, | ||
| errSink: errBuffer, | ||
| ); | ||
| final List<io.File> fileList = await clangTidy.computeChangedFiles(); | ||
| final List<io.File> fileList = await clangTidy.computeFilesOfInterest(); | ||
| expect(fileList.length, greaterThan(1000)); | ||
| }); | ||
|
|
||
|
|
@@ -205,10 +205,77 @@ Future<int> main(List<String> args) async { | |
| outSink: outBuffer, | ||
| errSink: errBuffer, | ||
| ); | ||
| final List<io.File> fileList = await clangTidy.computeChangedFiles(); | ||
| final List<io.File> fileList = await clangTidy.computeFilesOfInterest(); | ||
| expect(fileList.length, lessThan(300)); | ||
| }); | ||
|
|
||
| test('Sharding', () async { | ||
| final StringBuffer outBuffer = StringBuffer(); | ||
| final StringBuffer errBuffer = StringBuffer(); | ||
| final ClangTidy clangTidy = ClangTidy( | ||
| buildCommandsPath: io.File(buildCommands), | ||
| lintAll: true, | ||
| outSink: outBuffer, | ||
| errSink: errBuffer, | ||
| ); | ||
| Map<String, dynamic> makeBuildCommandEntry(String filePath) => <String, dynamic>{ | ||
| 'directory': '/unused', | ||
| 'command': '../../buildtools/mac-x64/clang/bin/clang $filePath', | ||
| 'file': filePath, | ||
| }; | ||
| final List<String> filePaths = () sync* { | ||
|
||
| for (int i = 0; i < 10; ++i) { | ||
| yield '/path/to/a/source_file_$i.cc'; | ||
| } | ||
| }().toList(); | ||
| final List<dynamic> buildCommandsData = | ||
| filePaths.map((String e) => makeBuildCommandEntry(e)).toList(); | ||
| final List<dynamic> shardBuildCommandsData = | ||
| filePaths.sublist(6).map((String e) => makeBuildCommandEntry(e)).toList(); | ||
|
|
||
| { | ||
| final List<Command> commands = await clangTidy.getLintCommandsForFiles( | ||
| buildCommandsData, | ||
| filePaths.map((String e) => io.File(e)).toList(), | ||
| <List<dynamic>>[shardBuildCommandsData], | ||
| 0, | ||
| ); | ||
| final Iterable<String> commandFilePaths = commands.map((Command e) => e.filePath); | ||
| expect(commands.length, equals(8)); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_0.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_1.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_2.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_3.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_4.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_5.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_6.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_7.cc'), false); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_8.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_9.cc'), false); | ||
| } | ||
| { | ||
| final List<Command> commands = await clangTidy.getLintCommandsForFiles( | ||
| buildCommandsData, | ||
| filePaths.map((String e) => io.File(e)).toList(), | ||
| <List<dynamic>>[shardBuildCommandsData], | ||
| 1, | ||
| ); | ||
|
|
||
| final Iterable<String> commandFilePaths = commands.map((Command e) => e.filePath); | ||
| expect(commands.length, equals(8)); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_0.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_1.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_2.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_3.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_4.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_5.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_6.cc'), false); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_7.cc'), true); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_8.cc'), false); | ||
| expect(commandFilePaths.contains('/path/to/a/source_file_9.cc'), true); | ||
| } | ||
| }); | ||
|
|
||
| test('No Commands are produced when no files changed', () async { | ||
| final StringBuffer outBuffer = StringBuffer(); | ||
| final StringBuffer errBuffer = StringBuffer(); | ||
|
|
@@ -226,9 +293,11 @@ Future<int> main(List<String> args) async { | |
| 'file': filePath, | ||
| }, | ||
| ]; | ||
| final List<Command> commands = await clangTidy.getLintCommandsForChangedFiles( | ||
| final List<Command> commands = await clangTidy.getLintCommandsForFiles( | ||
| buildCommandsData, | ||
| <io.File>[], | ||
| <List<dynamic>>[], | ||
| null, | ||
| ); | ||
|
|
||
| expect(commands, isEmpty); | ||
|
|
@@ -253,9 +322,11 @@ Future<int> main(List<String> args) async { | |
| 'file': filePath, | ||
| }, | ||
| ]; | ||
| final List<Command> commands = await clangTidy.getLintCommandsForChangedFiles( | ||
| final List<Command> commands = await clangTidy.getLintCommandsForFiles( | ||
| buildCommandsData, | ||
| <io.File>[io.File(filePath)], | ||
| <List<dynamic>>[], | ||
| null, | ||
| ); | ||
|
|
||
| expect(commands, isNotEmpty); | ||
|
|
||
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.
I know the rest of this file does this, but nit don't use
dynamicin null safe code, useObject?orObjectif you know it won't be 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.
done