Unverified Commit d2ba83d4 authored by Liam Appelbe's avatar Liam Appelbe Committed by GitHub

Use libraryFilters flag to speed up coverage collection (#104122)

* Use libraryFilters flag to speed up coverage collection

* Allow libraryNames to be null

* Unconditionally enable the reportLines flag

* Fix analysis errors
parent d9f3acb1
......@@ -112,16 +112,13 @@ Future<void> run(List<String> args) async {
Directory testDirectory;
CoverageCollector collector;
if (argResults['coverage'] as bool) {
// If we have a specified coverage directory then accept all libraries by
// setting libraryNames to null.
final Set<String> libraryNames = coverageDirectory != null ? null :
<String>{FlutterProject.current().manifest.appName};
collector = CoverageCollector(
packagesPath: globals.fs.path.normalize(globals.fs.path.absolute(argResults[_kOptionPackages] as String)),
libraryPredicate: (String libraryName) {
// If we have a specified coverage directory then accept all libraries.
if (coverageDirectory != null) {
return true;
}
final String projectName = FlutterProject.current().manifest.appName;
return libraryName.contains(projectName);
});
libraryNames: libraryNames);
if (!argResults.options.contains(_kOptionTestDirectory)) {
throwToolExit('Use of --coverage requires setting --test-directory');
}
......
......@@ -380,7 +380,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
final String projectName = flutterProject.manifest.appName;
collector = CoverageCollector(
verbose: !machine,
libraryPredicate: (String libraryName) => libraryName.contains(projectName),
libraryNames: <String>{projectName},
packagesPath: buildInfo.packagesPath
);
}
......
......@@ -19,7 +19,7 @@ import 'watcher.dart';
/// A class that collects code coverage data during test runs.
class CoverageCollector extends TestWatcher {
CoverageCollector({this.libraryPredicate, this.verbose = true, @required this.packagesPath});
CoverageCollector({this.libraryNames, this.verbose = true, @required this.packagesPath});
/// True when log messages should be emitted.
final bool verbose;
......@@ -31,9 +31,9 @@ class CoverageCollector extends TestWatcher {
/// Map of file path to coverage hit map for that file.
Map<String, coverage.HitMap> _globalHitmap;
/// Predicate function that returns true if the specified library URI should
/// be included in the computed coverage.
bool Function(String) libraryPredicate;
/// The names of the libraries to gather coverage for. If null, all libraries
/// will be accepted.
Set<String> libraryNames;
@override
Future<void> handleFinishedTest(TestDevice testDevice) async {
......@@ -83,7 +83,7 @@ class CoverageCollector extends TestWatcher {
Future<void> collectCoverageIsolate(Uri observatoryUri) async {
assert(observatoryUri != null);
_logMessage('collecting coverage data from $observatoryUri...');
final Map<String, dynamic> data = await collect(observatoryUri, libraryPredicate);
final Map<String, dynamic> data = await collect(observatoryUri, libraryNames);
if (data == null) {
throw Exception('Failed to collect coverage.');
}
......@@ -121,7 +121,7 @@ class CoverageCollector extends TestWatcher {
final Future<void> collectionComplete = testDevice.observatoryUri
.then((Uri observatoryUri) {
_logMessage('collecting coverage data from $testDevice at $observatoryUri...');
return collect(observatoryUri, libraryPredicate)
return collect(observatoryUri, libraryNames)
.then<void>((Map<String, dynamic> result) {
if (result == null) {
throw Exception('Failed to collect coverage.');
......@@ -237,31 +237,57 @@ Future<FlutterVmService> _defaultConnect(Uri serviceUri) {
serviceUri, compression: CompressionOptions.compressionOff, logger: globals.logger,);
}
Future<Map<String, dynamic>> collect(Uri serviceUri, bool Function(String) libraryPredicate, {
Future<Map<String, dynamic>> collect(Uri serviceUri, Set<String> libraryNames, {
bool waitPaused = false,
String debugName,
Future<FlutterVmService> Function(Uri) connector = _defaultConnect,
@visibleForTesting bool forceSequential = false,
}) async {
final FlutterVmService vmService = await connector(serviceUri);
final Map<String, dynamic> result = await _getAllCoverage(vmService.service, libraryPredicate, forceSequential);
final Map<String, dynamic> result = await _getAllCoverage(vmService.service, libraryNames, forceSequential);
await vmService.dispose();
return result;
}
Future<Map<String, dynamic>> _getAllCoverage(
vm_service.VmService service,
bool Function(String) libraryPredicate,
Set<String> libraryNames,
bool forceSequential,
) async {
final vm_service.Version version = await service.getVersion();
final bool reportLines = (version.major == 3 && version.minor >= 51) || version.major > 3;
final bool libraryFilters = (version.major == 3 && version.minor >= 57) || version.major > 3;
final vm_service.VM vm = await service.getVM();
final List<Map<String, dynamic>> coverage = <Map<String, dynamic>>[];
bool libraryPredicate(String libraryName) {
if (libraryNames == null) {
return true;
}
final Uri uri = Uri.parse(libraryName);
if (uri.scheme != 'package') {
return false;
}
final String scope = uri.path.split('/').first;
return libraryNames.contains(scope);
}
for (final vm_service.IsolateRef isolateRef in vm.isolates) {
if (isolateRef.isSystemIsolate) {
continue;
}
if (libraryFilters) {
final vm_service.SourceReport sourceReport = await service.getSourceReport(
isolateRef.id,
<String>['Coverage'],
forceCompile: true,
reportLines: true,
libraryFilters: libraryNames == null ? null : List<String>.from(
libraryNames.map((String name) => 'package:$name/')),
);
_buildCoverageMap(
<String, vm_service.Script>{},
<vm_service.SourceReport>[sourceReport],
coverage,
);
} else {
vm_service.ScriptList scriptList;
try {
scriptList = await service.getScripts(isolateRef.id);
......@@ -271,10 +297,10 @@ Future<Map<String, dynamic>> _getAllCoverage(
final List<Future<void>> futures = <Future<void>>[];
final Map<String, vm_service.Script> scripts = <String, vm_service.Script>{};
final Map<String, vm_service.SourceReport> sourceReports = <String, vm_service.SourceReport>{};
// For each ScriptRef loaded into the VM, load the corresponding Script and
// SourceReport object.
final List<vm_service.SourceReport> sourceReports = <vm_service.SourceReport>[];
// For each ScriptRef loaded into the VM, load the corresponding Script
// and SourceReport object.
for (final vm_service.ScriptRef script in scriptList.scripts) {
final String libraryUri = script.uri;
if (!libraryPredicate(libraryUri)) {
......@@ -286,28 +312,19 @@ Future<Map<String, dynamic>> _getAllCoverage(
<String>['Coverage'],
scriptId: scriptId,
forceCompile: true,
reportLines: reportLines ? true : null,
reportLines: true,
)
.then((vm_service.SourceReport report) {
sourceReports[scriptId] = report;
sourceReports.add(report);
});
if (forceSequential) {
await null;
}
futures.add(getSourceReport);
if (reportLines) {
continue;
}
final Future<void> getObject = service
.getObject(isolateRef.id, scriptId)
.then((vm_service.Obj response) {
final vm_service.Script script = response as vm_service.Script;
scripts[scriptId] = script;
});
futures.add(getObject);
}
await Future.wait(futures);
_buildCoverageMap(scripts, sourceReports, coverage, reportLines);
_buildCoverageMap(scripts, sourceReports, coverage);
}
}
return <String, dynamic>{'type': 'CodeCoverage', 'coverage': coverage};
}
......@@ -315,13 +332,11 @@ Future<Map<String, dynamic>> _getAllCoverage(
// Build a hitmap of Uri -> Line -> Hit Count for each script object.
void _buildCoverageMap(
Map<String, vm_service.Script> scripts,
Map<String, vm_service.SourceReport> sourceReports,
List<vm_service.SourceReport> sourceReports,
List<Map<String, dynamic>> coverage,
bool reportLines,
) {
final Map<String, Map<int, int>> hitMaps = <String, Map<int, int>>{};
for (final String scriptId in sourceReports.keys) {
final vm_service.SourceReport sourceReport = sourceReports[scriptId];
for (final vm_service.SourceReport sourceReport in sourceReports) {
for (final vm_service.SourceReportRange range in sourceReport.ranges) {
final vm_service.SourceReportCoverage coverage = range.coverage;
// Coverage reports may sometimes be null for a Script.
......@@ -335,24 +350,14 @@ void _buildCoverageMap(
final Map<int, int> hitMap = hitMaps[uri];
final List<int> hits = coverage.hits;
final List<int> misses = coverage.misses;
final List<dynamic> tokenPositions = scripts[scriptRef.id]?.tokenPosTable;
// The token positions can be null if the script has no lines that may be
// covered. It will also be null if reportLines is true.
if (tokenPositions == null && !reportLines) {
continue;
}
if (hits != null) {
for (final int hit in hits) {
final int line =
reportLines ? hit : _lineAndColumn(hit, tokenPositions)[0];
for (final int line in hits) {
final int current = hitMap[line] ?? 0;
hitMap[line] = current + 1;
}
}
if (misses != null) {
for (final int miss in misses) {
final int line =
reportLines ? miss : _lineAndColumn(miss, tokenPositions)[0];
for (final int line in misses) {
hitMap[line] ??= 0;
}
}
......@@ -363,29 +368,6 @@ void _buildCoverageMap(
});
}
// Binary search the token position table for the line and column which
// corresponds to each token position.
// The format of this table is described in https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md#script
List<int> _lineAndColumn(int position, List<dynamic> tokenPositions) {
int min = 0;
int max = tokenPositions.length;
while (min < max) {
final int mid = min + ((max - min) >> 1);
final List<int> row = (tokenPositions[mid] as List<dynamic>).cast<int>();
if (row[1] > position) {
max = mid;
} else {
for (int i = 1; i < row.length; i += 2) {
if (row[i] == position) {
return <int>[row.first, row[i + 1]];
}
}
min = mid + 1;
}
}
throw StateError('Unreachable');
}
// Returns a JSON hit map backward-compatible with pre-1.16.0 SDKs.
Map<String, dynamic> _toScriptCoverageJson(String scriptUri, Map<int, int> hitMap) {
final Map<String, dynamic> json = <String, dynamic>{};
......
......@@ -16,7 +16,7 @@ void main() {
requests: <VmServiceExpectation>[
FakeVmServiceRequest(
method: 'getVersion',
jsonResponse: Version(major: 3, minor: 50).toJson(),
jsonResponse: Version(major: 3, minor: 51).toJson(),
),
FakeVmServiceRequest(
method: 'getVM',
......@@ -42,7 +42,7 @@ void main() {
final Map<String, Object> result = await collect(
null,
(String predicate) => true,
<String>{'foo'},
connector: (Uri uri) async {
return fakeVmServiceHost.vmService;
},
......@@ -57,7 +57,7 @@ void main() {
requests: <VmServiceExpectation>[
FakeVmServiceRequest(
method: 'getVersion',
jsonResponse: Version(major: 3, minor: 50).toJson(),
jsonResponse: Version(major: 3, minor: 51).toJson(),
),
FakeVmServiceRequest(
method: 'getVM',
......@@ -75,7 +75,8 @@ void main() {
'isolateId': '1',
},
jsonResponse: ScriptList(scripts: <ScriptRef>[
ScriptRef(uri: 'foo.dart', id: '1'),
ScriptRef(uri: 'package:foo/foo.dart', id: '1'),
ScriptRef(uri: 'package:bar/bar.dart', id: '2'),
]).toJson(),
),
FakeVmServiceRequest(
......@@ -85,6 +86,7 @@ void main() {
'reports': <Object>['Coverage'],
'scriptId': '1',
'forceCompile': true,
'reportLines': true,
},
jsonResponse: SourceReport(
ranges: <SourceReportRange>[
......@@ -94,30 +96,134 @@ void main() {
endPos: 0,
compiled: true,
coverage: SourceReportCoverage(
hits: <int>[],
misses: <int>[],
hits: <int>[1, 3],
misses: <int>[2],
),
),
],
scripts: <ScriptRef>[
ScriptRef(
uri: 'foo.dart',
uri: 'package:foo/foo.dart',
id: '1',
),
],
).toJson(),
),
],
);
final Map<String, Object> result = await collect(
null,
<String>{'foo'},
connector: (Uri uri) async {
return fakeVmServiceHost.vmService;
},
);
expect(result, <String, Object>{
'type': 'CodeCoverage',
'coverage': <Object>[
<String, Object>{
'source': 'package:foo/foo.dart',
'script': <String, Object>{
'type': '@Script',
'fixedId': true,
'id': 'libraries/1/scripts/package%3Afoo%2Ffoo.dart',
'uri': 'package:foo/foo.dart',
'_kind': 'library',
},
'hits': <Object>[1, 1, 3, 1, 2, 0],
},
],
});
expect(fakeVmServiceHost.hasRemainingExpectations, false);
});
testWithoutContext('Coverage collector with null libraryNames accepts all libraries', () async {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[
FakeVmServiceRequest(
method: 'getVersion',
jsonResponse: Version(major: 3, minor: 51).toJson(),
),
FakeVmServiceRequest(
method: 'getObject',
method: 'getVM',
jsonResponse: (VM.parse(<String, Object>{})
..isolates = <IsolateRef>[
IsolateRef.parse(<String, Object>{
'id': '1',
}),
]
).toJson(),
),
FakeVmServiceRequest(
method: 'getScripts',
args: <String, Object>{
'isolateId': '1',
'objectId': '1',
},
jsonResponse: Script(
uri: 'foo.dart',
jsonResponse: ScriptList(scripts: <ScriptRef>[
ScriptRef(uri: 'package:foo/foo.dart', id: '1'),
ScriptRef(uri: 'package:bar/bar.dart', id: '2'),
]).toJson(),
),
FakeVmServiceRequest(
method: 'getSourceReport',
args: <String, Object>{
'isolateId': '1',
'reports': <Object>['Coverage'],
'scriptId': '1',
'forceCompile': true,
'reportLines': true,
},
jsonResponse: SourceReport(
ranges: <SourceReportRange>[
SourceReportRange(
scriptIndex: 0,
startPos: 0,
endPos: 0,
compiled: true,
coverage: SourceReportCoverage(
hits: <int>[1, 3],
misses: <int>[2],
),
),
],
scripts: <ScriptRef>[
ScriptRef(
uri: 'package:foo/foo.dart',
id: '1',
library: LibraryRef(name: '', id: '1111', uri: 'foo.dart'),
tokenPosTable: <List<int>>[],
),
],
).toJson(),
),
FakeVmServiceRequest(
method: 'getSourceReport',
args: <String, Object>{
'isolateId': '1',
'reports': <Object>['Coverage'],
'scriptId': '2',
'forceCompile': true,
'reportLines': true,
},
jsonResponse: SourceReport(
ranges: <SourceReportRange>[
SourceReportRange(
scriptIndex: 0,
startPos: 0,
endPos: 0,
compiled: true,
coverage: SourceReportCoverage(
hits: <int>[47, 21],
misses: <int>[32, 86],
),
),
],
scripts: <ScriptRef>[
ScriptRef(
uri: 'package:bar/bar.dart',
id: '2',
),
],
).toJson(),
),
],
......@@ -125,7 +231,7 @@ void main() {
final Map<String, Object> result = await collect(
null,
(String predicate) => true,
null,
connector: (Uri uri) async {
return fakeVmServiceHost.vmService;
},
......@@ -135,27 +241,38 @@ void main() {
'type': 'CodeCoverage',
'coverage': <Object>[
<String, Object>{
'source': 'foo.dart',
'source': 'package:foo/foo.dart',
'script': <String, Object>{
'type': '@Script',
'fixedId': true,
'id': 'libraries/1/scripts/foo.dart',
'uri': 'foo.dart',
'id': 'libraries/1/scripts/package%3Afoo%2Ffoo.dart',
'uri': 'package:foo/foo.dart',
'_kind': 'library',
},
'hits': <Object>[],
'hits': <Object>[1, 1, 3, 1, 2, 0],
},
<String, Object>{
'source': 'package:bar/bar.dart',
'script': <String, Object>{
'type': '@Script',
'fixedId': true,
'id': 'libraries/1/scripts/package%3Abar%2Fbar.dart',
'uri': 'package:bar/bar.dart',
'_kind': 'library',
},
'hits': <Object>[47, 1, 21, 1, 32, 0, 86, 0],
},
],
});
expect(fakeVmServiceHost.hasRemainingExpectations, false);
});
testWithoutContext('Coverage collector skips loading Script objects when reportLines is available', () async {
testWithoutContext('Coverage collector with libraryFilters', () async {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[
FakeVmServiceRequest(
method: 'getVersion',
jsonResponse: Version(major: 3, minor: 51).toJson(),
jsonResponse: Version(major: 3, minor: 57).toJson(),
),
FakeVmServiceRequest(
method: 'getVM',
......@@ -168,20 +285,87 @@ void main() {
).toJson(),
),
FakeVmServiceRequest(
method: 'getScripts',
method: 'getSourceReport',
args: <String, Object>{
'isolateId': '1',
'reports': <Object>['Coverage'],
'forceCompile': true,
'reportLines': true,
'libraryFilters': <Object>['package:foo/'],
},
jsonResponse: ScriptList(scripts: <ScriptRef>[
ScriptRef(uri: 'foo.dart', id: '1'),
]).toJson(),
jsonResponse: SourceReport(
ranges: <SourceReportRange>[
SourceReportRange(
scriptIndex: 0,
startPos: 0,
endPos: 0,
compiled: true,
coverage: SourceReportCoverage(
hits: <int>[1, 3],
misses: <int>[2],
),
),
],
scripts: <ScriptRef>[
ScriptRef(
uri: 'package:foo/foo.dart',
id: '1',
),
],
).toJson(),
),
],
);
final Map<String, Object> result = await collect(
null,
<String>{'foo'},
connector: (Uri uri) async {
return fakeVmServiceHost.vmService;
},
);
expect(result, <String, Object>{
'type': 'CodeCoverage',
'coverage': <Object>[
<String, Object>{
'source': 'package:foo/foo.dart',
'script': <String, Object>{
'type': '@Script',
'fixedId': true,
'id': 'libraries/1/scripts/package%3Afoo%2Ffoo.dart',
'uri': 'package:foo/foo.dart',
'_kind': 'library',
},
'hits': <Object>[1, 1, 3, 1, 2, 0],
},
],
});
expect(fakeVmServiceHost.hasRemainingExpectations, false);
});
testWithoutContext('Coverage collector with libraryFilters and null libraryNames', () async {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[
FakeVmServiceRequest(
method: 'getVersion',
jsonResponse: Version(major: 3, minor: 57).toJson(),
),
FakeVmServiceRequest(
method: 'getVM',
jsonResponse: (VM.parse(<String, Object>{})
..isolates = <IsolateRef>[
IsolateRef.parse(<String, Object>{
'id': '1',
}),
]
).toJson(),
),
FakeVmServiceRequest(
method: 'getSourceReport',
args: <String, Object>{
'isolateId': '1',
'reports': <Object>['Coverage'],
'scriptId': '1',
'forceCompile': true,
'reportLines': true,
},
......@@ -200,7 +384,7 @@ void main() {
],
scripts: <ScriptRef>[
ScriptRef(
uri: 'foo.dart',
uri: 'package:foo/foo.dart',
id: '1',
),
],
......@@ -211,7 +395,7 @@ void main() {
final Map<String, Object> result = await collect(
null,
(String predicate) => true,
null,
connector: (Uri uri) async {
return fakeVmServiceHost.vmService;
},
......@@ -221,12 +405,12 @@ void main() {
'type': 'CodeCoverage',
'coverage': <Object>[
<String, Object>{
'source': 'foo.dart',
'source': 'package:foo/foo.dart',
'script': <String, Object>{
'type': '@Script',
'fixedId': true,
'id': 'libraries/1/scripts/foo.dart',
'uri': 'foo.dart',
'id': 'libraries/1/scripts/package%3Afoo%2Ffoo.dart',
'uri': 'package:foo/foo.dart',
'_kind': 'library',
},
'hits': <Object>[1, 1, 3, 1, 2, 0],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment